diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de6bf224..02a0af67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,11 @@ jobs: rust: [1.76.0, stable, beta, nightly] env: RUSTFLAGS: "-D warnings" + # We use 'vcpkg' to install OpenSSL on Windows. + VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" + VCPKGRS_TRIPLET: x64-windows-release + # Ensure that OpenSSL is dynamically linked. + VCPKGRS_DYNAMIC: 1 steps: - name: Checkout repository uses: actions/checkout@v1 @@ -17,6 +22,16 @@ jobs: uses: hecrj/setup-rust-action@v2 with: rust-version: ${{ matrix.rust }} + - if: matrix.os == 'ubuntu-latest' + run: sudo apt-get install -y libssl-dev + - if: matrix.os == 'windows-latest' + id: vcpkg + uses: johnwason/vcpkg-action@v6 + with: + pkgs: openssl + triplet: ${{ env.VCPKGRS_TRIPLET }} + token: ${{ github.token }} + github-binarycache: true - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' @@ -37,6 +52,8 @@ jobs: uses: hecrj/setup-rust-action@v2 with: rust-version: "1.68.2" + - name: Install OpenSSL + run: sudo apt-get install -y libssl-dev - name: Install nightly Rust run: rustup install nightly - name: Check with minimal-versions diff --git a/Cargo.lock b/Cargo.lock index 58dfde03..ca7fb4b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,6 +232,7 @@ dependencies = [ "mock_instant", "moka", "octseq", + "openssl", "parking_lot", "proc-macro2", "rand", @@ -239,6 +240,7 @@ dependencies = [ "rstest", "rustls-pemfile", "rustversion", + "secrecy", "serde", "serde_json", "serde_test", @@ -284,6 +286,21 @@ dependencies = [ "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" version = "0.3.31" @@ -636,6 +653,44 @@ 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 = "overload" version = "0.1.1" @@ -703,6 +758,12 @@ 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" @@ -967,6 +1028,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" @@ -1336,6 +1406,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index b1bda94e..4c60ad9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,9 +33,11 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } +openssl = { version = "0.10.57", optional = true } # 0.10.57 upgrades to 'bitflags' 2.x proc-macro2 = { version = "1.0.69", optional = true } # Force proc-macro2 to at least 1.0.69 for minimal-version build ring = { version = "0.17", optional = true } rustversion = { version = "1", optional = true } +secrecy = { version = "0.10", optional = true } serde = { version = "1.0.130", optional = true, features = ["derive"] } siphasher = { version = "1", optional = true } smallvec = { version = "1.3", optional = true } @@ -47,24 +49,32 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil [features] default = ["std", "rand"] + +# Support for libraries bytes = ["dep:bytes", "octseq/bytes"] heapless = ["dep:heapless", "octseq/heapless"] -resolv = ["net", "smallvec", "unstable-client-transport"] -resolv-sync = ["resolv", "tokio/rt"] serde = ["dep:serde", "octseq/serde"] -sign = ["std"] smallvec = ["dep:smallvec", "octseq/smallvec"] std = ["dep:hashbrown", "bytes?/std", "octseq/std", "time/std"] + +# Cryptographic backends +ring = ["dep:ring"] +openssl = ["dep:openssl"] + +# Crate features net = ["bytes", "futures-util", "rand", "std", "tokio"] +resolv = ["net", "smallvec", "unstable-client-transport"] +resolv-sync = ["resolv", "tokio/rt"] tsig = ["bytes", "ring", "smallvec"] -validate = ["bytes", "std", "ring"] zonefile = ["bytes", "serde", "std"] # Unstable features unstable-client-transport = ["moka", "net", "tracing"] unstable-server-transport = ["arc-swap", "chrono/clock", "libc", "net", "siphasher", "tracing"] +unstable-sign = ["std", "dep:secrecy", "unstable-validate"] unstable-stelline = ["tokio/test-util", "tracing", "tracing-subscriber", "tsig", "unstable-client-transport", "unstable-server-transport", "zonefile"] -unstable-validator = ["validate", "zonefile", "unstable-client-transport"] +unstable-validate = ["bytes", "std", "ring"] +unstable-validator = ["unstable-validate", "zonefile", "unstable-client-transport"] unstable-xfr = ["net"] unstable-zonetree = ["futures-util", "parking_lot", "rustversion", "serde", "std", "tokio", "tracing", "unstable-xfr", "zonefile"] diff --git a/src/lib.rs b/src/lib.rs index 6d6cfd34..0d0a4a2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,14 +36,14 @@ #![cfg_attr(not(feature = "resolv"), doc = "* resolv:")] //! An asynchronous DNS resolver based on the //! [Tokio](https://tokio.rs/) async runtime. -#![cfg_attr(feature = "sign", doc = "* [sign]:")] -#![cfg_attr(not(feature = "sign"), doc = "* sign:")] +#![cfg_attr(feature = "unstable-sign", doc = "* [sign]:")] +#![cfg_attr(not(feature = "unstable-sign"), doc = "* sign:")] //! Experimental support for DNSSEC signing. #![cfg_attr(feature = "tsig", doc = "* [tsig]:")] #![cfg_attr(not(feature = "tsig"), doc = "* tsig:")] //! Support for securing DNS transactions with TSIG records. -#![cfg_attr(feature = "validate", doc = "* [validate]:")] -#![cfg_attr(not(feature = "validate"), doc = "* validate:")] +#![cfg_attr(feature = "unstable-validate", doc = "* [validate]:")] +#![cfg_attr(not(feature = "unstable-validate"), doc = "* validate:")] //! Experimental support for DNSSEC validation. #![cfg_attr(feature = "unstable-validator", doc = "* [validator]:")] #![cfg_attr(not(feature = "unstable-validator"), doc = "* validator:")] @@ -61,61 +61,79 @@ //! //! # Reference of feature flags //! -//! The following is the complete list of the feature flags with the -//! exception of unstable features which are described below. +//! Several feature flags simply enable support for other crates, e.g. by +//! adding `impl`s for their types. They are optional and do not introduce +//! new functionality into this crate. //! //! * `bytes`: Enables using the types `Bytes` and `BytesMut` from the //! [bytes](https://github.com/tokio-rs/bytes) crate as octet sequences. -//! * `chrono`: Adds the [chrono](https://github.com/chronotope/chrono) -//! crate as a dependency. This adds support for generating serial numbers -//! from time stamps. +//! //! * `heapless`: enables the use of the `Vec` type from the //! [heapless](https://github.com/japaric/heapless) crate as octet //! sequences. -//! * `interop`: Activate interoperability tests that rely on other software -//! to be installed in the system (currently NSD and dig) and will fail if -//! it isn’t. This feature is not meaningful for users of the crate. +//! +//! * `smallvec`: enables the use of the `Smallvec` type from the +//! [smallvec](https://github.com/servo/rust-smallvec) crate as octet +//! sequences. +//! +//! Some flags enable support for specific kinds of operations that are not +//! otherwise possible. They are gated as they may not always be necessary +//! and they may introduce new dependencies. +//! +//! * `chrono`: Adds the [chrono](https://github.com/chronotope/chrono) +//! crate as a dependency. This adds support for generating serial numbers +//! from time stamps. +//! //! * `rand`: Enables a number of methods that rely on a random number //! generator being available in the system. -//! * `resolv`: Enables the asynchronous stub resolver via the -#![cfg_attr(feature = "resolv", doc = " [resolv]")] -#![cfg_attr(not(feature = "resolv"), doc = " resolv")] -//! module. -//! * `resolv-sync`: Enables the synchronous version of the stub resolver. -//! * `ring`: Enables crypto functionality via the -//! [ring](https://github.com/briansmith/ring) crate. +//! //! * `serde`: Enables serde serialization for a number of basic types. -//! * `sign`: basic DNSSEC signing support. This will enable the -#![cfg_attr(feature = "sign", doc = " [sign]")] -#![cfg_attr(not(feature = "sign"), doc = " sign")] -//! module and requires the `std` feature. Note that this will not directly -//! enable actual signing. For that you will also need to pick a crypto -//! module via an additional feature. Currently we only support the `ring` -//! module, but support for OpenSSL is coming soon. +//! //! * `siphasher`: enables the dependency on the //! [siphasher](https://github.com/jedisct1/rust-siphash) crate which allows //! generating and checking hashes in [standard server //! cookies][crate::base::opt::cookie::StandardServerCookie]. -//! * `smallvec`: enables the use of the `Smallvec` type from the -//! [smallvec](https://github.com/servo/rust-smallvec) crate as octet -//! sequences. +//! //! * `std`: support for the Rust std library. This feature is enabled by //! default. +//! +//! A special case here is cryptographic backends. Certain modules (e.g. for +//! DNSSEC signing and validation) require a backend to provide cryptography. +//! At least one such module should be enabled. +//! +//! * `openssl`: Enables crypto functionality via OpenSSL through the +//! [rust-openssl](https://github.com/sfackler/rust-openssl) crate. +//! +//! * `ring`: Enables crypto functionality via the +//! [ring](https://github.com/briansmith/ring) crate. +//! +//! Some flags represent entire categories of functionality within this crate. +//! Each flag is associated with a particular module. Note that some of these +//! modules are under heavy development, and so have unstable feature flags +//! which are categorized separately. +//! +//! * `net`: Enables sending and receiving DNS messages via the +#![cfg_attr(feature = "net", doc = " [net]")] +#![cfg_attr(not(feature = "net"), doc = " net")] +//! module. +//! +//! * `resolv`: Enables the asynchronous stub resolver via the +#![cfg_attr(feature = "resolv", doc = " [resolv]")] +#![cfg_attr(not(feature = "resolv"), doc = " resolv")] +//! module. +//! +//! * `resolv-sync`: Enables the synchronous version of the stub resolver. +//! //! * `tsig`: support for signing and validating message exchanges via TSIG //! signatures. This enables the #![cfg_attr(feature = "tsig", doc = " [tsig]")] #![cfg_attr(not(feature = "tsig"), doc = " tsig")] -//! module and currently pulls in the -//! `bytes`, `ring`, and `smallvec` features. -//! * `validate`: basic DNSSEC validation support. This feature enables the -#![cfg_attr(feature = "validate", doc = " [validate]")] -#![cfg_attr(not(feature = "validate"), doc = " validate")] -//! module and currently also enables the `std` and `ring` -//! features. +//! module and currently enables `bytes`, `ring`, and `smallvec`. +//! //! * `zonefile`: reading and writing of zonefiles. This feature enables the #![cfg_attr(feature = "zonefile", doc = " [zonefile]")] #![cfg_attr(not(feature = "zonefile"), doc = " zonefile")] -//! module and currently also enables the `bytes` and `std` features. +//! module and currently also enables `bytes`, `serde`, and `std`. //! //! # Unstable features //! @@ -137,6 +155,16 @@ //! a client perspective; primarily the `net::client` module. //! * `unstable-server-transport`: receiving and sending DNS messages from //! a server perspective; primarily the `net::server` module. +//! * `unstable-sign`: basic DNSSEC signing support. This will enable the +#![cfg_attr(feature = "unstable-sign", doc = " [sign]")] +#![cfg_attr(not(feature = "unstable-sign"), doc = " sign")] +//! module and requires the `std` feature. In order to actually perform any +//! signing, also enable one or more cryptographic backend modules (`ring` +//! and `openssl`). +//! * `unstable-validate`: basic DNSSEC validation support. This enables the +#![cfg_attr(feature = "unstable-validate", doc = " [validate]")] +#![cfg_attr(not(feature = "unstable-validate"), doc = " validate")] +//! module and currently also enables the `std` and `ring` features. //! * `unstable-validator`: a DNSSEC validator, primarily the `validator` //! and the `net::client::validator` modules. //! * `unstable-xfr`: zone transfer related functionality.. diff --git a/src/sign/bytes.rs b/src/sign/bytes.rs new file mode 100644 index 00000000..6393a0ac --- /dev/null +++ b/src/sign/bytes.rs @@ -0,0 +1,509 @@ +//! A generic representation of secret keys. + +use core::{fmt, str}; + +use secrecy::{ExposeSecret, SecretBox}; +use std::boxed::Box; +use std::vec::Vec; + +use crate::base::iana::SecAlg; +use crate::utils::base64; +use crate::validate::RsaPublicKeyBytes; + +//----------- SecretKeyBytes ------------------------------------------------- + +/// A secret key expressed as raw bytes. +/// +/// This is a low-level generic representation of a secret key from any one of +/// the commonly supported signature algorithms. It is useful for abstracting +/// over most cryptographic implementations, and it provides functionality for +/// importing and exporting keys from and to the disk. +/// +/// # Serialization +/// +/// This type can be used to interact with private keys stored in the format +/// popularized by BIND. The format is rather under-specified, but examples +/// of it are available in [RFC 5702], [RFC 6605], and [RFC 8080]. +/// +/// [RFC 5702]: https://www.rfc-editor.org/rfc/rfc5702 +/// [RFC 6605]: https://www.rfc-editor.org/rfc/rfc6605 +/// [RFC 8080]: https://www.rfc-editor.org/rfc/rfc8080 +/// +/// In this format, a private key is a line-oriented text file. Each line is +/// either blank (having only whitespace) or a key-value entry. Entries have +/// three components: a key, an ASCII colon, and a value. Keys contain ASCII +/// text (except for colons) and values contain any data up to the end of the +/// line. Whitespace at either end of the key and the value will be ignored. +/// +/// Every file begins with two entries: +/// +/// - `Private-key-format` specifies the format of the file. The RFC examples +/// above use version 1.2 (serialised `v1.2`), but recent versions of BIND +/// have defined a new version 1.3 (serialized `v1.3`). +/// +/// This value should be treated akin to Semantic Versioning principles. If +/// the major version (the first number) is unknown to a parser, it should +/// fail, since it does not know the layout of the following fields. If the +/// minor version is greater than what a parser is expecting, it should +/// ignore any following fields it did not expect. +/// +/// - `Algorithm` specifies the signing algorithm used by the private key. +/// This can affect the format of later fields. The value consists of two +/// whitespace-separated words: the first is the ASCII decimal number of the +/// algorithm (see [`SecAlg`]); the second is the name of the algorithm in +/// ASCII parentheses (with no whitespace inside). Valid combinations are: +/// +/// - `8 (RSASHA256)`: RSA with the SHA-256 digest. +/// - `10 (RSASHA512)`: RSA with the SHA-512 digest. +/// - `13 (ECDSAP256SHA256)`: ECDSA with the P-256 curve and SHA-256 digest. +/// - `14 (ECDSAP384SHA384)`: ECDSA with the P-384 curve and SHA-384 digest. +/// - `15 (ED25519)`: Ed25519. +/// - `16 (ED448)`: Ed448. +/// +/// The value of every following entry is a Base64-encoded string of variable +/// length, using the RFC 4648 variant (i.e. with `+` and `/`, and `=` for +/// padding). It is unclear whether padding is required or optional. +/// +/// In the case of RSA, the following fields are defined (their conventional +/// symbolic names are also provided): +/// +/// - `Modulus` (n) +/// - `PublicExponent` (e) +/// - `PrivateExponent` (d) +/// - `Prime1` (p) +/// - `Prime2` (q) +/// - `Exponent1` (d_p) +/// - `Exponent2` (d_q) +/// - `Coefficient` (q_inv) +/// +/// For all other algorithms, there is a single `PrivateKey` field, whose +/// contents should be interpreted as: +/// +/// - For ECDSA, the private scalar of the key, as a fixed-width byte string +/// interpreted as a big-endian integer. +/// +/// - For EdDSA, the private scalar of the key, as a fixed-width byte string. +pub enum SecretKeyBytes { + /// An RSA/SHA-256 keypair. + RsaSha256(RsaSecretKeyBytes), + + /// An ECDSA P-256/SHA-256 keypair. + /// + /// The private key is a single 32-byte big-endian integer. + EcdsaP256Sha256(SecretBox<[u8; 32]>), + + /// An ECDSA P-384/SHA-384 keypair. + /// + /// The private key is a single 48-byte big-endian integer. + EcdsaP384Sha384(SecretBox<[u8; 48]>), + + /// An Ed25519 keypair. + /// + /// The private key is a single 32-byte string. + Ed25519(SecretBox<[u8; 32]>), + + /// An Ed448 keypair. + /// + /// The private key is a single 57-byte string. + Ed448(SecretBox<[u8; 57]>), +} + +//--- Inspection + +impl SecretKeyBytes { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } +} + +//--- Converting to and from the BIND format + +impl SecretKeyBytes { + /// Serialize this secret key in the conventional format used by BIND. + /// + /// The key is formatted in the private key v1.2 format and written to the + /// given formatter. See the type-level documentation for a description + /// of this format. + pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { + writeln!(w, "Private-key-format: v1.2")?; + match self { + Self::RsaSha256(k) => { + writeln!(w, "Algorithm: 8 (RSASHA256)")?; + k.format_as_bind(w) + } + + Self::EcdsaP256Sha256(s) => { + let s = s.expose_secret(); + writeln!(w, "Algorithm: 13 (ECDSAP256SHA256)")?; + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + } + + Self::EcdsaP384Sha384(s) => { + let s = s.expose_secret(); + writeln!(w, "Algorithm: 14 (ECDSAP384SHA384)")?; + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + } + + Self::Ed25519(s) => { + let s = s.expose_secret(); + writeln!(w, "Algorithm: 15 (ED25519)")?; + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + } + + Self::Ed448(s) => { + let s = s.expose_secret(); + writeln!(w, "Algorithm: 16 (ED448)")?; + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + } + } + } + + /// Display this secret key in the conventional format used by BIND. + /// + /// This is a simple wrapper around [`Self::format_as_bind()`]. + pub fn display_as_bind(&self) -> impl fmt::Display + '_ { + struct Display<'a>(&'a SecretKeyBytes); + impl fmt::Display for Display<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.format_as_bind(f) + } + } + Display(self) + } + + /// Parse a secret key from the conventional format used by BIND. + /// + /// This parser supports the private key v1.2 format, but it should be + /// compatible with any future v1.x key. See the type-level documentation + /// for a description of this format. + pub fn parse_from_bind(data: &str) -> Result { + /// Parse private keys for most algorithms (except RSA). + fn parse_pkey( + mut data: &str, + ) -> Result, BindFormatError> { + // Look for the 'PrivateKey' field. + while let Some((key, val, rest)) = parse_bind_entry(data)? { + data = rest; + + if key != "PrivateKey" { + continue; + } + + // TODO: Evaluate security of 'base64::decode()'. + let val: Vec = base64::decode(val) + .map_err(|_| BindFormatError::Misformatted)?; + let val: Box<[u8]> = val.into_boxed_slice(); + let val: Box<[u8; N]> = val + .try_into() + .map_err(|_| BindFormatError::Misformatted)?; + + return Ok(val.into()); + } + + // The 'PrivateKey' field was not found. + Err(BindFormatError::Misformatted) + } + + // The first line should specify the key format. + let (_, _, data) = parse_bind_entry(data)? + .filter(|&(k, v, _)| { + k == "Private-key-format" + && v.strip_prefix("v1.") + .and_then(|minor| minor.parse::().ok()) + .map_or(false, |minor| minor >= 2) + }) + .ok_or(BindFormatError::UnsupportedFormat)?; + + // The second line should specify the algorithm. + let (_, val, data) = parse_bind_entry(data)? + .filter(|&(k, _, _)| k == "Algorithm") + .ok_or(BindFormatError::Misformatted)?; + + // Parse the algorithm. + let mut words = val.split_whitespace(); + let code = words + .next() + .and_then(|code| code.parse::().ok()) + .ok_or(BindFormatError::Misformatted)?; + let name = words.next().ok_or(BindFormatError::Misformatted)?; + if words.next().is_some() { + return Err(BindFormatError::Misformatted); + } + + match (code, name) { + (8, "(RSASHA256)") => { + RsaSecretKeyBytes::parse_from_bind(data).map(Self::RsaSha256) + } + (13, "(ECDSAP256SHA256)") => { + parse_pkey(data).map(Self::EcdsaP256Sha256) + } + (14, "(ECDSAP384SHA384)") => { + parse_pkey(data).map(Self::EcdsaP384Sha384) + } + (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), + (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), + _ => Err(BindFormatError::UnsupportedAlgorithm), + } + } +} + +//----------- RsaSecretKeyBytes --------------------------------------------------- + +/// An RSA secret key expressed as raw bytes. +/// +/// All fields here are arbitrary-precision integers in big-endian format. +/// The public values, `n` and `e`, must not have leading zeros; the remaining +/// values may be padded with leading zeros. +pub struct RsaSecretKeyBytes { + /// The public modulus. + pub n: Box<[u8]>, + + /// The public exponent. + pub e: Box<[u8]>, + + /// The private exponent. + pub d: SecretBox<[u8]>, + + /// The first prime factor of `d`. + pub p: SecretBox<[u8]>, + + /// The second prime factor of `d`. + pub q: SecretBox<[u8]>, + + /// The exponent corresponding to the first prime factor of `d`. + pub d_p: SecretBox<[u8]>, + + /// The exponent corresponding to the second prime factor of `d`. + pub d_q: SecretBox<[u8]>, + + /// The inverse of the second prime factor modulo the first. + pub q_i: SecretBox<[u8]>, +} + +//--- Conversion to and from the BIND format + +impl RsaSecretKeyBytes { + /// Serialize this secret key in the conventional format used by BIND. + /// + /// The key is formatted in the private key v1.2 format and written to the + /// given formatter. Note that the header and algorithm lines are not + /// written. See the type-level documentation of [`SecretKeyBytes`] for a + /// description of this format. + pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { + w.write_str("Modulus: ")?; + writeln!(w, "{}", base64::encode_display(&self.n))?; + w.write_str("PublicExponent: ")?; + writeln!(w, "{}", base64::encode_display(&self.e))?; + w.write_str("PrivateExponent: ")?; + writeln!(w, "{}", base64::encode_display(&self.d.expose_secret()))?; + w.write_str("Prime1: ")?; + writeln!(w, "{}", base64::encode_display(&self.p.expose_secret()))?; + w.write_str("Prime2: ")?; + writeln!(w, "{}", base64::encode_display(&self.q.expose_secret()))?; + w.write_str("Exponent1: ")?; + writeln!(w, "{}", base64::encode_display(&self.d_p.expose_secret()))?; + w.write_str("Exponent2: ")?; + writeln!(w, "{}", base64::encode_display(&self.d_q.expose_secret()))?; + w.write_str("Coefficient: ")?; + writeln!(w, "{}", base64::encode_display(&self.q_i.expose_secret()))?; + Ok(()) + } + + /// Display this secret key in the conventional format used by BIND. + /// + /// This is a simple wrapper around [`Self::format_as_bind()`]. + pub fn display_as_bind(&self) -> impl fmt::Display + '_ { + struct Display<'a>(&'a RsaSecretKeyBytes); + impl fmt::Display for Display<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.format_as_bind(f) + } + } + Display(self) + } + + /// Parse a secret key from the conventional format used by BIND. + /// + /// This parser supports the private key v1.2 format, but it should be + /// compatible with any future v1.x key. Note that the header and + /// algorithm lines are ignored. See the type-level documentation of + /// [`SecretKeyBytes`] for a description of this format. + pub fn parse_from_bind(mut data: &str) -> Result { + let mut n = None; + let mut e = None; + let mut d = None; + let mut p = None; + let mut q = None; + let mut d_p = None; + let mut d_q = None; + let mut q_i = None; + + while let Some((key, val, rest)) = parse_bind_entry(data)? { + let field = match key { + "Modulus" => &mut n, + "PublicExponent" => &mut e, + "PrivateExponent" => &mut d, + "Prime1" => &mut p, + "Prime2" => &mut q, + "Exponent1" => &mut d_p, + "Exponent2" => &mut d_q, + "Coefficient" => &mut q_i, + _ => { + data = rest; + continue; + } + }; + + if field.is_some() { + // This field has already been filled. + return Err(BindFormatError::Misformatted); + } + + let buffer: Vec = base64::decode(val) + .map_err(|_| BindFormatError::Misformatted)?; + + *field = Some(buffer.into_boxed_slice()); + data = rest; + } + + for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { + if field.is_none() { + // A field was missing. + return Err(BindFormatError::Misformatted); + } + } + + Ok(Self { + n: n.unwrap(), + e: e.unwrap(), + d: d.unwrap().into(), + p: p.unwrap().into(), + q: q.unwrap().into(), + d_p: d_p.unwrap().into(), + d_q: d_q.unwrap().into(), + q_i: q_i.unwrap().into(), + }) + } +} + +//--- Into + +impl<'a> From<&'a RsaSecretKeyBytes> for RsaPublicKeyBytes { + fn from(value: &'a RsaSecretKeyBytes) -> Self { + RsaPublicKeyBytes { + n: value.n.clone(), + e: value.e.clone(), + } + } +} + +//----------- Helpers for parsing the BIND format ---------------------------- + +/// Extract the next key-value pair in a BIND-format private key file. +fn parse_bind_entry( + data: &str, +) -> Result, BindFormatError> { + // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. + + // Trim any pending newlines. + let data = data.trim_start(); + + // Stop if there's no more data. + if data.is_empty() { + return Ok(None); + } + + // Get the first line (NOTE: CR LF is handled later). + let (line, rest) = data.split_once('\n').unwrap_or((data, "")); + + // Split the line by a colon. + let (key, val) = + line.split_once(':').ok_or(BindFormatError::Misformatted)?; + + // Trim the key and value (incl. for CR LFs). + Ok(Some((key.trim(), val.trim(), rest))) +} + +//============ Error types =================================================== + +//----------- BindFormatError ------------------------------------------------ + +/// An error in loading a [`SecretKeyBytes`] from the conventional DNS format. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum BindFormatError { + /// The key file uses an unsupported version of the format. + UnsupportedFormat, + + /// The key file did not follow the DNS format correctly. + Misformatted, + + /// The key file used an unsupported algorithm. + UnsupportedAlgorithm, +} + +//--- Display + +impl fmt::Display for BindFormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedFormat => "unsupported format", + Self::Misformatted => "misformatted key file", + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +//--- Error + +impl std::error::Error for BindFormatError {} + +//============ Tests ========================================================= + +#[cfg(test)] +mod tests { + use std::{string::ToString, vec::Vec}; + + use crate::base::iana::SecAlg; + + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 60616), + (SecAlg::ECDSAP256SHA256, 42253), + (SecAlg::ECDSAP384SHA384, 33566), + (SecAlg::ED25519, 56037), + (SecAlg::ED448, 7379), + ]; + + #[test] + fn secret_from_dns() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = super::SecretKeyBytes::parse_from_bind(&data).unwrap(); + assert_eq!(key.algorithm(), algorithm); + } + } + + #[test] + fn secret_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = super::SecretKeyBytes::parse_from_bind(&data).unwrap(); + let same = key.display_as_bind().to_string(); + let data = data.lines().collect::>(); + let same = same.lines().collect::>(); + assert_eq!(data, same); + } + } +} diff --git a/src/sign/common.rs b/src/sign/common.rs new file mode 100644 index 00000000..fe0fd111 --- /dev/null +++ b/src/sign/common.rs @@ -0,0 +1,274 @@ +//! DNSSEC signing using built-in backends. +//! +//! This backend supports all the algorithms supported by Ring and OpenSSL, +//! depending on whether the respective crate features are enabled. See the +//! documentation for each backend for more information. + +use core::fmt; +use std::sync::Arc; + +use ::ring::rand::SystemRandom; + +use crate::{ + base::iana::SecAlg, + validate::{PublicKeyBytes, Signature}, +}; + +use super::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; + +#[cfg(feature = "openssl")] +use super::openssl; + +#[cfg(feature = "ring")] +use super::ring; + +//----------- KeyPair -------------------------------------------------------- + +/// A key pair based on a built-in backend. +/// +/// This supports any built-in backend (currently, that is OpenSSL and Ring, +/// if their respective feature flags are enabled). Wherever possible, it +/// will prefer the Ring backend over OpenSSL -- but for more uncommon or +/// insecure algorithms, that Ring does not support, OpenSSL must be used. +pub enum KeyPair { + /// A key backed by Ring. + #[cfg(feature = "ring")] + Ring(ring::KeyPair), + + /// A key backed by OpenSSL. + #[cfg(feature = "openssl")] + OpenSSL(openssl::KeyPair), +} + +//--- Conversion to and from bytes + +impl KeyPair { + /// Import a key pair from bytes. + pub fn from_bytes( + secret: &SecretKeyBytes, + public: &PublicKeyBytes, + ) -> Result { + // Prefer Ring if it is available. + #[cfg(feature = "ring")] + match public { + PublicKeyBytes::RsaSha1(k) + | PublicKeyBytes::RsaSha1Nsec3Sha1(k) + | PublicKeyBytes::RsaSha256(k) + | PublicKeyBytes::RsaSha512(k) + if k.n.len() >= 2048 / 8 => + { + let rng = Arc::new(SystemRandom::new()); + let key = ring::KeyPair::from_bytes(secret, public, rng)?; + return Ok(Self::Ring(key)); + } + + PublicKeyBytes::EcdsaP256Sha256(_) + | PublicKeyBytes::EcdsaP384Sha384(_) => { + let rng = Arc::new(SystemRandom::new()); + let key = ring::KeyPair::from_bytes(secret, public, rng)?; + return Ok(Self::Ring(key)); + } + + PublicKeyBytes::Ed25519(_) => { + let rng = Arc::new(SystemRandom::new()); + let key = ring::KeyPair::from_bytes(secret, public, rng)?; + return Ok(Self::Ring(key)); + } + + _ => {} + } + + // Fall back to OpenSSL. + #[cfg(feature = "openssl")] + return Ok(Self::OpenSSL(openssl::KeyPair::from_bytes( + secret, public, + )?)); + + // Otherwise fail. + #[allow(unreachable_code)] + Err(FromBytesError::UnsupportedAlgorithm) + } +} + +//--- SignRaw + +impl SignRaw for KeyPair { + fn algorithm(&self) -> SecAlg { + match self { + #[cfg(feature = "ring")] + Self::Ring(key) => key.algorithm(), + #[cfg(feature = "openssl")] + Self::OpenSSL(key) => key.algorithm(), + } + } + + fn raw_public_key(&self) -> PublicKeyBytes { + match self { + #[cfg(feature = "ring")] + Self::Ring(key) => key.raw_public_key(), + #[cfg(feature = "openssl")] + Self::OpenSSL(key) => key.raw_public_key(), + } + } + + fn sign_raw(&self, data: &[u8]) -> Result { + match self { + #[cfg(feature = "ring")] + Self::Ring(key) => key.sign_raw(data), + #[cfg(feature = "openssl")] + Self::OpenSSL(key) => key.sign_raw(data), + } + } +} + +//----------- generate() ----------------------------------------------------- + +/// Generate a new secret key for the given algorithm. +pub fn generate( + params: GenerateParams, +) -> Result<(SecretKeyBytes, PublicKeyBytes), GenerateError> { + // Use Ring if it is available. + #[cfg(feature = "ring")] + if matches!( + ¶ms, + GenerateParams::EcdsaP256Sha256 + | GenerateParams::EcdsaP384Sha384 + | GenerateParams::Ed25519 + ) { + let rng = ::ring::rand::SystemRandom::new(); + return Ok(ring::generate(params, &rng)?); + } + + // Fall back to OpenSSL. + #[cfg(feature = "openssl")] + { + let key = openssl::generate(params)?; + return Ok((key.to_bytes(), key.raw_public_key())); + } + + // Otherwise fail. + #[allow(unreachable_code)] + Err(GenerateError::UnsupportedAlgorithm) +} + +//============ Error Types =================================================== + +//----------- FromBytesError ----------------------------------------------- + +/// An error in importing a key pair from bytes. +#[derive(Clone, Debug)] +pub enum FromBytesError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The key's parameters were invalid. + InvalidKey, + + /// The implementation does not allow such weak keys. + WeakKey, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversions + +#[cfg(feature = "ring")] +impl From for FromBytesError { + fn from(value: ring::FromBytesError) -> Self { + match value { + ring::FromBytesError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + ring::FromBytesError::InvalidKey => Self::InvalidKey, + ring::FromBytesError::WeakKey => Self::WeakKey, + } + } +} + +#[cfg(feature = "openssl")] +impl From for FromBytesError { + fn from(value: openssl::FromBytesError) -> Self { + match value { + openssl::FromBytesError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + openssl::FromBytesError::InvalidKey => Self::InvalidKey, + openssl::FromBytesError::Implementation => Self::Implementation, + } + } +} + +//--- Formatting + +impl fmt::Display for FromBytesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + Self::WeakKey => "key too weak to be supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for FromBytesError {} + +//----------- GenerateError -------------------------------------------------- + +/// An error in generating a key pair. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +#[cfg(feature = "ring")] +impl From for GenerateError { + fn from(value: ring::GenerateError) -> Self { + match value { + ring::GenerateError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + ring::GenerateError::Implementation => Self::Implementation, + } + } +} + +#[cfg(feature = "openssl")] +impl From for GenerateError { + fn from(value: openssl::GenerateError) -> Self { + match value { + openssl::GenerateError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + openssl::GenerateError::Implementation => Self::Implementation, + } + } +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for GenerateError {} diff --git a/src/sign/key.rs b/src/sign/key.rs deleted file mode 100644 index da938578..00000000 --- a/src/sign/key.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::base::iana::SecAlg; -use crate::base::name::ToName; -use crate::rdata::{Dnskey, Ds}; - -pub trait SigningKey { - type Octets: AsRef<[u8]>; - type Signature: AsRef<[u8]>; - type Error; - - fn dnskey(&self) -> Result, Self::Error>; - fn ds( - &self, - owner: N, - ) -> Result, Self::Error>; - - fn algorithm(&self) -> Result { - self.dnskey().map(|dnskey| dnskey.algorithm()) - } - - fn key_tag(&self) -> Result { - self.dnskey().map(|dnskey| dnskey.key_tag()) - } - - fn sign(&self, data: &[u8]) -> Result; -} - -impl<'a, K: SigningKey> SigningKey for &'a K { - type Octets = K::Octets; - type Signature = K::Signature; - type Error = K::Error; - - fn dnskey(&self) -> Result, Self::Error> { - (*self).dnskey() - } - fn ds( - &self, - owner: N, - ) -> Result, Self::Error> { - (*self).ds(owner) - } - - fn algorithm(&self) -> Result { - (*self).algorithm() - } - - fn key_tag(&self) -> Result { - (*self).key_tag() - } - - fn sign(&self, data: &[u8]) -> Result { - (*self).sign(data) - } -} diff --git a/src/sign/mod.rs b/src/sign/mod.rs index d87acca0..7adbb607 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -1,10 +1,414 @@ //! DNSSEC signing. //! //! **This module is experimental and likely to change significantly.** -#![cfg(feature = "sign")] -#![cfg_attr(docsrs, doc(cfg(feature = "sign")))] +//! +//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of +//! a DNS record served by a security-aware name server. Signatures can be +//! made "online" (in an authoritative name server while it is running) or +//! "offline" (outside of a name server). Once generated, signatures can be +//! serialized as DNS records and stored alongside the authenticated records. +//! +//! Signatures can be generated using a [`SigningKey`], which combines +//! cryptographic key material with additional information that defines how +//! the key should be used. [`SigningKey`] relies on a cryptographic backend +//! to provide the underlying signing operation (e.g. [`common::KeyPair`]). +//! +//! # Example Usage +//! +//! At the moment, only "low-level" signing is supported. +//! +//! ``` +//! # use domain::sign::*; +//! # use domain::base::Name; +//! // Generate a new Ed25519 key. +//! let params = GenerateParams::Ed25519; +//! let (sec_bytes, pub_bytes) = common::generate(params).unwrap(); +//! +//! // Parse the key into Ring or OpenSSL. +//! let key_pair = common::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! +//! // Associate the key with important metadata. +//! let owner: Name> = "www.example.org.".parse().unwrap(); +//! let flags = 257; // key signing key +//! let key = SigningKey::new(owner, flags, key_pair); +//! +//! // Access the public key (with metadata). +//! let pub_key = key.public_key(); +//! println!("{:?}", pub_key); +//! +//! // Sign arbitrary byte sequences with the key. +//! let sig = key.raw_secret_key().sign_raw(b"Hello, World!").unwrap(); +//! println!("{:?}", sig); +//! ``` +//! +//! It is also possible to import keys stored on disk in the conventional BIND +//! format. +//! +//! ``` +//! # use domain::base::iana::SecAlg; +//! # use domain::{sign::*, validate}; +//! // Load an Ed25519 key named 'Ktest.+015+56037'. +//! let base = "test-data/dnssec-keys/Ktest.+015+56037"; +//! let sec_text = std::fs::read_to_string(format!("{base}.private")).unwrap(); +//! let sec_bytes = SecretKeyBytes::parse_from_bind(&sec_text).unwrap(); +//! let pub_text = std::fs::read_to_string(format!("{base}.key")).unwrap(); +//! let pub_key = validate::Key::>::parse_from_bind(&pub_text).unwrap(); +//! +//! // Parse the key into Ring or OpenSSL. +//! let key_pair = common::KeyPair::from_bytes(&sec_bytes, pub_key.raw_public_key()).unwrap(); +//! +//! // Associate the key with important metadata. +//! let key = SigningKey::new(pub_key.owner().clone(), pub_key.flags(), key_pair); +//! +//! // Check that the owner, algorithm, and key tag matched expectations. +//! assert_eq!(key.owner().to_string(), "test"); +//! assert_eq!(key.algorithm(), SecAlg::ED25519); +//! assert_eq!(key.public_key().key_tag(), 56037); +//! ``` +//! +//! # Cryptography +//! +//! This crate supports OpenSSL and Ring for performing cryptography. These +//! cryptographic backends are gated on the `openssl` and `ring` features, +//! respectively. They offer mostly equivalent functionality, but OpenSSL +//! supports a larger set of signing algorithms (and, for RSA keys, supports +//! weaker key sizes). A [`common`] backend is provided for users that wish +//! to use either or both backends at runtime. +//! +//! Each backend module (`openssl`, `ring`, and `common`) exposes a `KeyPair` +//! type, representing a cryptographic key that can be used for signing, and a +//! `generate()` function for creating new keys. +//! +//! Users can choose to bring their own cryptography by providing their own +//! `KeyPair` type that implements [`SignRaw`]. Note that `async` signing +//! (useful for interacting with cryptographic hardware like HSMs) is not +//! currently supported. +//! +//! While each cryptographic backend can support a limited number of signature +//! algorithms, even the types independent of a cryptographic backend (e.g. +//! [`SecretKeyBytes`] and [`GenerateParams`]) support a limited number of +//! algorithms. Even with custom cryptographic backends, this module can only +//! support these algorithms. +//! +//! # Importing and Exporting +//! +//! The [`SecretKeyBytes`] type is a generic representation of a secret key as +//! a byte slice. While it does not offer any cryptographic functionality, it +//! is useful to transfer secret keys stored in memory, independent of any +//! cryptographic backend. +//! +//! The `KeyPair` types of the cryptographic backends in this module each +//! support a `from_bytes()` function that parses the generic representation +//! into a functional cryptographic key. Importantly, these functions require +//! both the public and private keys to be provided -- the pair are verified +//! for consistency. In some cases, it may also be possible to serialize an +//! existing cryptographic key back to the generic bytes representation. +//! +//! [`SecretKeyBytes`] also supports importing and exporting keys from and to +//! the conventional private-key format popularized by BIND. This format is +//! used by a variety of tools for storing DNSSEC keys on disk. See the +//! type-level documentation for a specification of the format. -pub mod key; -//pub mod openssl; -pub mod records; +#![cfg(feature = "unstable-sign")] +#![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] + +use core::fmt; + +use crate::{ + base::{iana::SecAlg, Name}, + validate, +}; + +pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; + +mod bytes; +pub use self::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; + +pub mod common; +pub mod openssl; pub mod ring; + +//----------- SigningKey ----------------------------------------------------- + +/// A signing key. +/// +/// This associates important metadata with a raw cryptographic secret key. +pub struct SigningKey { + /// The owner of the key. + owner: Name, + + /// The flags associated with the key. + /// + /// These flags are stored in the DNSKEY record. + flags: u16, + + /// The raw private key. + inner: Inner, +} + +//--- Construction + +impl SigningKey { + /// Construct a new signing key manually. + pub fn new(owner: Name, flags: u16, inner: Inner) -> Self { + Self { + owner, + flags, + inner, + } + } +} + +//--- Inspection + +impl SigningKey { + /// The owner name attached to the key. + pub fn owner(&self) -> &Name { + &self.owner + } + + /// The flags attached to the key. + pub fn flags(&self) -> u16 { + self.flags + } + + /// The raw secret key. + pub fn raw_secret_key(&self) -> &Inner { + &self.inner + } + + /// Whether this is a zone signing key. + /// + /// From [RFC 4034, section 2.1.1]: + /// + /// > Bit 7 of the Flags field is the Zone Key flag. If bit 7 has value + /// > 1, then the DNSKEY record holds a DNS zone key, and the DNSKEY RR's + /// > owner name MUST be the name of a zone. If bit 7 has value 0, then + /// > the DNSKEY record holds some other type of DNS public key and MUST + /// > NOT be used to verify RRSIGs that cover RRsets. + /// + /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 + pub fn is_zone_signing_key(&self) -> bool { + self.flags & (1 << 8) != 0 + } + + /// Whether this key has been revoked. + /// + /// From [RFC 5011, section 3]: + /// + /// > Bit 8 of the DNSKEY Flags field is designated as the 'REVOKE' flag. + /// > If this bit is set to '1', AND the resolver sees an RRSIG(DNSKEY) + /// > signed by the associated key, then the resolver MUST consider this + /// > key permanently invalid for all purposes except for validating the + /// > revocation. + /// + /// [RFC 5011, section 3]: https://datatracker.ietf.org/doc/html/rfc5011#section-3 + pub fn is_revoked(&self) -> bool { + self.flags & (1 << 7) != 0 + } + + /// Whether this is a secure entry point. + /// + /// From [RFC 4034, section 2.1.1]: + /// + /// > Bit 15 of the Flags field is the Secure Entry Point flag, described + /// > in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a + /// > key intended for use as a secure entry point. This flag is only + /// > intended to be a hint to zone signing or debugging software as to + /// > the intended use of this DNSKEY record; validators MUST NOT alter + /// > their behavior during the signature validation process in any way + /// > based on the setting of this bit. This also means that a DNSKEY RR + /// > with the SEP bit set would also need the Zone Key flag set in order + /// > to be able to generate signatures legally. A DNSKEY RR with the SEP + /// > set and the Zone Key flag not set MUST NOT be used to verify RRSIGs + /// > that cover RRsets. + /// + /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 + /// [RFC3757]: https://datatracker.ietf.org/doc/html/rfc3757 + pub fn is_secure_entry_point(&self) -> bool { + self.flags & 1 != 0 + } + + /// The signing algorithm used. + pub fn algorithm(&self) -> SecAlg { + self.inner.algorithm() + } + + /// The associated public key. + pub fn public_key(&self) -> validate::Key<&Octs> + where + Octs: AsRef<[u8]>, + { + let owner = Name::from_octets(self.owner.as_octets()).unwrap(); + validate::Key::new(owner, self.flags, self.inner.raw_public_key()) + } + + /// The associated raw public key. + pub fn raw_public_key(&self) -> PublicKeyBytes { + self.inner.raw_public_key() + } +} + +// TODO: Conversion to and from key files + +//----------- SignRaw -------------------------------------------------------- + +/// Low-level signing functionality. +/// +/// Types that implement this trait own a private key and can sign arbitrary +/// information (in the form of slices of bytes). +/// +/// Implementing types should validate keys during construction, so that +/// signing does not fail due to invalid keys. If the implementing type +/// allows [`sign_raw()`] to be called on unvalidated keys, it will have to +/// check the validity of the key for every signature; this is unnecessary +/// overhead when many signatures have to be generated. +/// +/// [`sign_raw()`]: SignRaw::sign_raw() +pub trait SignRaw { + /// The signature algorithm used. + /// + /// See [RFC 8624, section 3.1] for IETF implementation recommendations. + /// + /// [RFC 8624, section 3.1]: https://datatracker.ietf.org/doc/html/rfc8624#section-3.1 + fn algorithm(&self) -> SecAlg; + + /// The raw public key. + /// + /// This can be used to verify produced signatures. It must use the same + /// algorithm as returned by [`algorithm()`]. + /// + /// [`algorithm()`]: Self::algorithm() + fn raw_public_key(&self) -> PublicKeyBytes; + + /// Sign the given bytes. + /// + /// # Errors + /// + /// See [`SignError`] for a discussion of possible failure cases. To the + /// greatest extent possible, the implementation should check for failure + /// cases beforehand and prevent them (e.g. when the keypair is created). + fn sign_raw(&self, data: &[u8]) -> Result; +} + +//----------- GenerateParams ------------------------------------------------- + +/// Parameters for generating a secret key. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum GenerateParams { + /// Generate an RSA/SHA-256 keypair. + RsaSha256 { + /// The number of bits in the public modulus. + /// + /// A ~3000-bit key corresponds to a 128-bit security level. However, + /// RSA is mostly used with 2048-bit keys. Some backends (like Ring) + /// do not support smaller key sizes than that. + /// + /// For more information about security levels, see [NIST SP 800-57 + /// part 1 revision 5], page 54, table 2. + /// + /// [NIST SP 800-57 part 1 revision 5]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf + bits: u32, + }, + + /// Generate an ECDSA P-256/SHA-256 keypair. + EcdsaP256Sha256, + + /// Generate an ECDSA P-384/SHA-384 keypair. + EcdsaP384Sha384, + + /// Generate an Ed25519 keypair. + Ed25519, + + /// An Ed448 keypair. + Ed448, +} + +//--- Inspection + +impl GenerateParams { + /// The algorithm of the generated key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::EcdsaP256Sha256 => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384 => SecAlg::ECDSAP384SHA384, + Self::Ed25519 => SecAlg::ED25519, + Self::Ed448 => SecAlg::ED448, + } + } +} + +//============ Error Types =================================================== + +//----------- SignError ------------------------------------------------------ + +/// A signature failure. +/// +/// In case such an error occurs, callers should stop using the key pair they +/// attempted to sign with. If such an error occurs with every key pair they +/// have available, or if such an error occurs with a freshly-generated key +/// pair, they should use a different cryptographic implementation. If that +/// is not possible, they must forego signing entirely. +/// +/// # Failure Cases +/// +/// Signing should be an infallible process. There are three considerable +/// failure cases for it: +/// +/// - The secret key was invalid (e.g. its parameters were inconsistent). +/// +/// Such a failure would mean that all future signing (with this key) will +/// also fail. In any case, the implementations provided by this crate try +/// to verify the key (e.g. by checking the consistency of the private and +/// public components) before any signing occurs, largely ruling this class +/// of errors out. +/// +/// - Not enough randomness could be obtained. This applies to signature +/// algorithms which use randomization (e.g. RSA and ECDSA). +/// +/// On the vast majority of platforms, randomness can always be obtained. +/// The [`getrandom` crate documentation][getrandom] notes: +/// +/// > If an error does occur, then it is likely that it will occur on every +/// > call to getrandom, hence after the first successful call one can be +/// > reasonably confident that no errors will occur. +/// +/// [getrandom]: https://docs.rs/getrandom +/// +/// Thus, in case such a failure occurs, all future signing will probably +/// also fail. +/// +/// - Not enough memory could be allocated. +/// +/// Signature algorithms have a small memory overhead, so an out-of-memory +/// condition means that the program is nearly out of allocatable space. +/// +/// Callers who do not expect allocations to fail (i.e. who are using the +/// standard memory allocation routines, not their `try_` variants) will +/// likely panic shortly after such an error. +/// +/// Callers who are aware of their memory usage will likely restrict it far +/// before they get to this point. Systems running at near-maximum load +/// tend to quickly become unresponsive and staggeringly slow. If memory +/// usage is an important consideration, programs will likely cap it before +/// the system reaches e.g. 90% memory use. +/// +/// As such, memory allocation failure should never really occur. It is far +/// more likely that one of the other errors has occurred. +/// +/// It may be reasonable to panic in any such situation, since each kind of +/// error is essentially unrecoverable. However, applications where signing +/// is an optional step, or where crashing is prohibited, may wish to recover +/// from such an error differently (e.g. by foregoing signatures or informing +/// an operator). +#[derive(Clone, Debug)] +pub struct SignError; + +impl fmt::Display for SignError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("could not create a cryptographic signature") + } +} + +impl std::error::Error for SignError {} diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index c49512b7..a7250081 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,58 +1,570 @@ -//! Key and Signer using OpenSSL. +//! DNSSEC signing using OpenSSL. +//! +//! This backend supports the following algorithms: +//! +//! - RSA/SHA-256 (512-bit keys or larger) +//! - ECDSA P-256/SHA-256 +//! - ECDSA P-384/SHA-384 +//! - Ed25519 +//! - Ed448 + #![cfg(feature = "openssl")] #![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] -use std::vec::Vec; -use openssl::error::ErrorStack; -use openssl::hash::MessageDigest; -use openssl::pkey::{PKey, Private}; -use openssl::sha::sha256; -use openssl::sign::Signer as OpenSslSigner; -use unwrap::unwrap; -use crate::base::iana::DigestAlg; -use crate::base::name::ToDname; -use crate::base::octets::Compose; -use crate::rdata::{Ds, Dnskey}; -use super::key::SigningKey; +use core::fmt; +use std::{boxed::Box, vec::Vec}; +use openssl::{ + bn::BigNum, + ecdsa::EcdsaSig, + error::ErrorStack, + pkey::{self, PKey, Private}, +}; +use secrecy::ExposeSecret; -pub struct Key { - dnskey: Dnskey>, - key: PKey, - digest: MessageDigest, +use crate::{ + base::iana::SecAlg, + validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}, +}; + +use super::{ + GenerateParams, RsaSecretKeyBytes, SecretKeyBytes, SignError, SignRaw, +}; + +//----------- KeyPair -------------------------------------------------------- + +/// A key pair backed by OpenSSL. +pub struct KeyPair { + /// The algorithm used by the key. + algorithm: SecAlg, + + /// The private key. + pkey: PKey, } -impl SigningKey for Key { - type Octets = Vec; - type Signature = Vec; - type Error = ErrorStack; +//--- Conversion to and from bytes - fn dnskey(&self) -> Result, Self::Error> { - Ok(self.dnskey.clone()) +impl KeyPair { + /// Import a key pair from bytes into OpenSSL. + pub fn from_bytes( + secret: &SecretKeyBytes, + public: &PublicKeyBytes, + ) -> Result { + fn num(slice: &[u8]) -> Result { + let mut v = BigNum::new()?; + v.copy_from_slice(slice)?; + Ok(v) + } + + fn secure_num(slice: &[u8]) -> Result { + let mut v = BigNum::new_secure()?; + v.copy_from_slice(slice)?; + Ok(v) + } + + let pkey = match (secret, public) { + (SecretKeyBytes::RsaSha256(s), PublicKeyBytes::RsaSha256(p)) => { + // Ensure that the public and private key match. + if p != &RsaPublicKeyBytes::from(s) { + return Err(FromBytesError::InvalidKey); + } + + let n = num(&s.n)?; + let e = num(&s.e)?; + let d = secure_num(s.d.expose_secret())?; + let p = secure_num(s.p.expose_secret())?; + let q = secure_num(s.q.expose_secret())?; + let d_p = secure_num(s.d_p.expose_secret())?; + let d_q = secure_num(s.d_q.expose_secret())?; + let q_i = secure_num(s.q_i.expose_secret())?; + + // NOTE: The 'openssl' crate doesn't seem to expose + // 'EVP_PKEY_fromdata', which could be used to replace the + // deprecated methods called here. + + let key = openssl::rsa::Rsa::from_private_components( + n, e, d, p, q, d_p, d_q, q_i, + )?; + + if !key.check_key()? { + return Err(FromBytesError::InvalidKey); + } + + PKey::from_rsa(key)? + } + + ( + SecretKeyBytes::EcdsaP256Sha256(s), + PublicKeyBytes::EcdsaP256Sha256(p), + ) => { + use openssl::{bn, ec, nid}; + + let mut ctx = bn::BigNumContext::new_secure()?; + let group = nid::Nid::X9_62_PRIME256V1; + let group = ec::EcGroup::from_curve_name(group)?; + let n = secure_num(s.expose_secret().as_slice())?; + let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx)?; + let k = ec::EcKey::from_private_components(&group, &n, &p)?; + k.check_key().map_err(|_| FromBytesError::InvalidKey)?; + PKey::from_ec_key(k)? + } + + ( + SecretKeyBytes::EcdsaP384Sha384(s), + PublicKeyBytes::EcdsaP384Sha384(p), + ) => { + use openssl::{bn, ec, nid}; + + let mut ctx = bn::BigNumContext::new_secure()?; + let group = nid::Nid::SECP384R1; + let group = ec::EcGroup::from_curve_name(group)?; + let n = secure_num(s.expose_secret().as_slice())?; + let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx)?; + let k = ec::EcKey::from_private_components(&group, &n, &p)?; + k.check_key().map_err(|_| FromBytesError::InvalidKey)?; + PKey::from_ec_key(k)? + } + + (SecretKeyBytes::Ed25519(s), PublicKeyBytes::Ed25519(p)) => { + use openssl::memcmp; + + let id = pkey::Id::ED25519; + let s = s.expose_secret(); + let k = PKey::private_key_from_raw_bytes(s, id)?; + if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { + k + } else { + return Err(FromBytesError::InvalidKey); + } + } + + (SecretKeyBytes::Ed448(s), PublicKeyBytes::Ed448(p)) => { + use openssl::memcmp; + + let id = pkey::Id::ED448; + let s = s.expose_secret(); + let k = PKey::private_key_from_raw_bytes(s, id)?; + if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { + k + } else { + return Err(FromBytesError::InvalidKey); + } + } + + // The public and private key types did not match. + _ => return Err(FromBytesError::InvalidKey), + }; + + Ok(Self { + algorithm: secret.algorithm(), + pkey, + }) } - fn ds( - &self, - owner: N - ) -> Result, Self::Error> { - let mut buf = Vec::new(); - unwrap!(owner.compose_canonical(&mut buf)); - unwrap!(self.dnskey.compose_canonical(&mut buf)); - let digest = Vec::from(sha256(&buf).as_ref()); - Ok(Ds::new( - self.key_tag()?, - self.dnskey.algorithm(), - DigestAlg::Sha256, - digest, - )) - } - - fn sign(&self, data: &[u8]) -> Result { - let mut signer = OpenSslSigner::new( - self.digest, &self.key - )?; - signer.update(data)?; - signer.sign_to_vec() + /// Export the secret key into bytes. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn to_bytes(&self) -> SecretKeyBytes { + // TODO: Consider security implications of secret data in 'Vec's. + match self.algorithm { + SecAlg::RSASHA256 => { + let key = self.pkey.rsa().unwrap(); + SecretKeyBytes::RsaSha256(RsaSecretKeyBytes { + n: key.n().to_vec().into(), + e: key.e().to_vec().into(), + d: key.d().to_vec().into(), + p: key.p().unwrap().to_vec().into(), + q: key.q().unwrap().to_vec().into(), + d_p: key.dmp1().unwrap().to_vec().into(), + d_q: key.dmq1().unwrap().to_vec().into(), + q_i: key.iqmp().unwrap().to_vec().into(), + }) + } + SecAlg::ECDSAP256SHA256 => { + let key = self.pkey.ec_key().unwrap(); + let key = key.private_key().to_vec_padded(32).unwrap(); + let key: Box<[u8; 32]> = key.try_into().unwrap(); + SecretKeyBytes::EcdsaP256Sha256(key.into()) + } + SecAlg::ECDSAP384SHA384 => { + let key = self.pkey.ec_key().unwrap(); + let key = key.private_key().to_vec_padded(48).unwrap(); + let key: Box<[u8; 48]> = key.try_into().unwrap(); + SecretKeyBytes::EcdsaP384Sha384(key.into()) + } + SecAlg::ED25519 => { + let key = self.pkey.raw_private_key().unwrap(); + let key: Box<[u8; 32]> = key.try_into().unwrap(); + SecretKeyBytes::Ed25519(key.into()) + } + SecAlg::ED448 => { + let key = self.pkey.raw_private_key().unwrap(); + let key: Box<[u8; 57]> = key.try_into().unwrap(); + SecretKeyBytes::Ed448(key.into()) + } + _ => unreachable!(), + } } } +//--- Signing + +impl KeyPair { + fn sign(&self, data: &[u8]) -> Result, ErrorStack> { + use openssl::hash::MessageDigest; + use openssl::sign::Signer; + + match self.algorithm { + SecAlg::RSASHA256 => { + let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; + s.set_rsa_padding(openssl::rsa::Padding::PKCS1)?; + s.sign_oneshot_to_vec(data) + } + + SecAlg::ECDSAP256SHA256 => { + let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature)?; + let mut r = signature.r().to_vec_padded(32)?; + let mut s = signature.s().to_vec_padded(32)?; + r.append(&mut s); + Ok(r) + } + SecAlg::ECDSAP384SHA384 => { + let mut s = Signer::new(MessageDigest::sha384(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature)?; + let mut r = signature.r().to_vec_padded(48)?; + let mut s = signature.s().to_vec_padded(48)?; + r.append(&mut s); + Ok(r) + } + + SecAlg::ED25519 => { + let mut s = Signer::new_without_digest(&self.pkey)?; + s.sign_oneshot_to_vec(data) + } + SecAlg::ED448 => { + let mut s = Signer::new_without_digest(&self.pkey)?; + s.sign_oneshot_to_vec(data) + } + + _ => unreachable!(), + } + } +} + +//--- SignRaw + +impl SignRaw for KeyPair { + fn algorithm(&self) -> SecAlg { + self.algorithm + } + + fn raw_public_key(&self) -> PublicKeyBytes { + match self.algorithm { + SecAlg::RSASHA256 => { + let key = self.pkey.rsa().unwrap(); + PublicKeyBytes::RsaSha256(RsaPublicKeyBytes { + n: key.n().to_vec().into(), + e: key.e().to_vec().into(), + }) + } + SecAlg::ECDSAP256SHA256 => { + let key = self.pkey.ec_key().unwrap(); + let form = openssl::ec::PointConversionForm::UNCOMPRESSED; + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let key = key + .public_key() + .to_bytes(key.group(), form, &mut ctx) + .unwrap(); + PublicKeyBytes::EcdsaP256Sha256(key.try_into().unwrap()) + } + SecAlg::ECDSAP384SHA384 => { + let key = self.pkey.ec_key().unwrap(); + let form = openssl::ec::PointConversionForm::UNCOMPRESSED; + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let key = key + .public_key() + .to_bytes(key.group(), form, &mut ctx) + .unwrap(); + PublicKeyBytes::EcdsaP384Sha384(key.try_into().unwrap()) + } + SecAlg::ED25519 => { + let key = self.pkey.raw_public_key().unwrap(); + PublicKeyBytes::Ed25519(key.try_into().unwrap()) + } + SecAlg::ED448 => { + let key = self.pkey.raw_public_key().unwrap(); + PublicKeyBytes::Ed448(key.try_into().unwrap()) + } + _ => unreachable!(), + } + } + + fn sign_raw(&self, data: &[u8]) -> Result { + let signature = self + .sign(data) + .map(Vec::into_boxed_slice) + .map_err(|_| SignError)?; + + match self.algorithm { + SecAlg::RSASHA256 => Ok(Signature::RsaSha256(signature)), + + SecAlg::ECDSAP256SHA256 => signature + .try_into() + .map(Signature::EcdsaP256Sha256) + .map_err(|_| SignError), + SecAlg::ECDSAP384SHA384 => signature + .try_into() + .map(Signature::EcdsaP384Sha384) + .map_err(|_| SignError), + + SecAlg::ED25519 => signature + .try_into() + .map(Signature::Ed25519) + .map_err(|_| SignError), + SecAlg::ED448 => signature + .try_into() + .map(Signature::Ed448) + .map_err(|_| SignError), + + _ => unreachable!(), + } + } +} + +//----------- generate() ----------------------------------------------------- + +/// Generate a new secret key for the given algorithm. +pub fn generate(params: GenerateParams) -> Result { + let algorithm = params.algorithm(); + let pkey = match params { + GenerateParams::RsaSha256 { bits } => { + openssl::rsa::Rsa::generate(bits).and_then(PKey::from_rsa)? + } + GenerateParams::EcdsaP256Sha256 => { + let group = openssl::nid::Nid::X9_62_PRIME256V1; + let group = openssl::ec::EcGroup::from_curve_name(group)?; + PKey::from_ec_key(openssl::ec::EcKey::generate(&group)?)? + } + GenerateParams::EcdsaP384Sha384 => { + let group = openssl::nid::Nid::SECP384R1; + let group = openssl::ec::EcGroup::from_curve_name(group)?; + PKey::from_ec_key(openssl::ec::EcKey::generate(&group)?)? + } + GenerateParams::Ed25519 => PKey::generate_ed25519()?, + GenerateParams::Ed448 => PKey::generate_ed448()?, + }; + + Ok(KeyPair { algorithm, pkey }) +} + +//============ Error Types =================================================== + +//----------- FromBytesError ----------------------------------------------- + +/// An error in importing a key pair from bytes into OpenSSL. +#[derive(Clone, Debug)] +pub enum FromBytesError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The key's parameters were invalid. + InvalidKey, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +impl From for FromBytesError { + fn from(_: ErrorStack) -> Self { + Self::Implementation + } +} + +//--- Formatting + +impl fmt::Display for FromBytesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for FromBytesError {} + +//----------- GenerateError -------------------------------------------------- + +/// An error in generating a key pair with OpenSSL. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +impl From for GenerateError { + fn from(_: ErrorStack) -> Self { + Self::Implementation + } +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for GenerateError {} + +//============ Tests ========================================================= + +#[cfg(test)] +mod tests { + use std::{string::ToString, vec::Vec}; + + use crate::{ + base::iana::SecAlg, + sign::{GenerateParams, SecretKeyBytes, SignRaw}, + validate::Key, + }; + + use super::KeyPair; + + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 60616), + (SecAlg::ECDSAP256SHA256, 42253), + (SecAlg::ECDSAP384SHA384, 33566), + (SecAlg::ED25519, 56037), + (SecAlg::ED448, 7379), + ]; + + #[test] + fn generate() { + for &(algorithm, _) in KEYS { + let params = match algorithm { + SecAlg::RSASHA256 => GenerateParams::RsaSha256 { bits: 3072 }, + SecAlg::ECDSAP256SHA256 => GenerateParams::EcdsaP256Sha256, + SecAlg::ECDSAP384SHA384 => GenerateParams::EcdsaP384Sha384, + SecAlg::ED25519 => GenerateParams::Ed25519, + SecAlg::ED448 => GenerateParams::Ed448, + _ => unreachable!(), + }; + + let _ = super::generate(params).unwrap(); + } + } + + #[test] + fn generated_roundtrip() { + for &(algorithm, _) in KEYS { + let params = match algorithm { + SecAlg::RSASHA256 => GenerateParams::RsaSha256 { bits: 3072 }, + SecAlg::ECDSAP256SHA256 => GenerateParams::EcdsaP256Sha256, + SecAlg::ECDSAP384SHA384 => GenerateParams::EcdsaP384Sha384, + SecAlg::ED25519 => GenerateParams::Ed25519, + SecAlg::ED448 => GenerateParams::Ed448, + _ => unreachable!(), + }; + + let key = super::generate(params).unwrap(); + let gen_key = key.to_bytes(); + let pub_key = key.raw_public_key(); + let equiv = KeyPair::from_bytes(&gen_key, &pub_key).unwrap(); + assert!(key.pkey.public_eq(&equiv.pkey)); + } + } + + #[test] + fn imported_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = Key::>::parse_from_bind(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + + let key = KeyPair::from_bytes(&gen_key, pub_key).unwrap(); + let same = key.to_bytes().display_as_bind().to_string(); + + let data = data.lines().collect::>(); + let same = same.lines().collect::>(); + assert_eq!(data, same); + } + } + + #[test] + fn public_key() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = Key::>::parse_from_bind(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); + + let key = KeyPair::from_bytes(&gen_key, pub_key).unwrap(); + + assert_eq!(key.raw_public_key(), *pub_key); + } + } + + #[test] + fn sign() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = Key::>::parse_from_bind(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); + + let key = KeyPair::from_bytes(&gen_key, pub_key).unwrap(); + + let _ = key.sign_raw(b"Hello, World!").unwrap(); + } + } +} diff --git a/src/sign/ring.rs b/src/sign/ring.rs index bf4614f2..d1e29c39 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -1,140 +1,443 @@ -//! Key and Signer using ring. +//! DNSSEC signing using `ring`. +//! +//! This backend supports the following algorithms: +//! +//! - RSA/SHA-256 (2048-bit keys or larger) +//! - ECDSA P-256/SHA-256 +//! - ECDSA P-384/SHA-384 +//! - Ed25519 + #![cfg(feature = "ring")] #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] -use super::key::SigningKey; -use crate::base::iana::{DigestAlg, SecAlg}; -use crate::base::name::ToName; -use crate::base::rdata::ComposeRecordData; -use crate::rdata::{Dnskey, Ds}; -#[cfg(feature = "bytes")] -use bytes::Bytes; -use octseq::builder::infallible; -use ring::digest; -use ring::error::Unspecified; -use ring::rand::SecureRandom; +use core::fmt; +use std::{boxed::Box, sync::Arc, vec::Vec}; + use ring::signature::{ - EcdsaKeyPair, Ed25519KeyPair, KeyPair, RsaEncoding, RsaKeyPair, - Signature as RingSignature, ECDSA_P256_SHA256_FIXED_SIGNING, + EcdsaKeyPair, Ed25519KeyPair, KeyPair as _, RsaKeyPair, }; -use std::vec::Vec; +use secrecy::ExposeSecret; -pub struct Key<'a> { - dnskey: Dnskey>, - key: RingKey, - rng: &'a dyn SecureRandom, -} +use crate::{ + base::iana::SecAlg, + validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}, +}; -#[allow(dead_code, clippy::large_enum_variant)] -enum RingKey { - Ecdsa(EcdsaKeyPair), +use super::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; + +//----------- KeyPair -------------------------------------------------------- + +/// A key pair backed by `ring`. +pub enum KeyPair { + /// An RSA/SHA-256 keypair. + RsaSha256 { + key: RsaKeyPair, + rng: Arc, + }, + + /// An ECDSA P-256/SHA-256 keypair. + EcdsaP256Sha256 { + key: EcdsaKeyPair, + rng: Arc, + }, + + /// An ECDSA P-384/SHA-384 keypair. + EcdsaP384Sha384 { + key: EcdsaKeyPair, + rng: Arc, + }, + + /// An Ed25519 keypair. Ed25519(Ed25519KeyPair), - Rsa(RsaKeyPair, &'static dyn RsaEncoding), } -impl<'a> Key<'a> { - pub fn throwaway_13( - flags: u16, - rng: &'a dyn SecureRandom, - ) -> Result { - let pkcs8 = EcdsaKeyPair::generate_pkcs8( - &ECDSA_P256_SHA256_FIXED_SIGNING, - rng, - )?; - let keypair = EcdsaKeyPair::from_pkcs8( - &ECDSA_P256_SHA256_FIXED_SIGNING, - pkcs8.as_ref(), - rng, - )?; - let public_key = keypair.public_key().as_ref()[1..].into(); - Ok(Key { - dnskey: Dnskey::new( - flags, - 3, - SecAlg::ECDSAP256SHA256, - public_key, - ) - .expect("long key"), - key: RingKey::Ecdsa(keypair), - rng, +//--- Conversion from bytes + +impl KeyPair { + /// Import a key pair from bytes into OpenSSL. + pub fn from_bytes( + secret: &SecretKeyBytes, + public: &PublicKeyBytes, + rng: Arc, + ) -> Result { + match (secret, public) { + (SecretKeyBytes::RsaSha256(s), PublicKeyBytes::RsaSha256(p)) => { + // Ensure that the public and private key match. + if p != &RsaPublicKeyBytes::from(s) { + return Err(FromBytesError::InvalidKey); + } + + // Ensure that the key is strong enough. + if p.n.len() < 2048 / 8 { + return Err(FromBytesError::WeakKey); + } + + let components = ring::rsa::KeyPairComponents { + public_key: ring::rsa::PublicKeyComponents { + n: s.n.as_ref(), + e: s.e.as_ref(), + }, + d: s.d.expose_secret(), + p: s.p.expose_secret(), + q: s.q.expose_secret(), + dP: s.d_p.expose_secret(), + dQ: s.d_q.expose_secret(), + qInv: s.q_i.expose_secret(), + }; + ring::signature::RsaKeyPair::from_components(&components) + .map_err(|_| FromBytesError::InvalidKey) + .map(|key| Self::RsaSha256 { key, rng }) + } + + ( + SecretKeyBytes::EcdsaP256Sha256(s), + PublicKeyBytes::EcdsaP256Sha256(p), + ) => { + let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; + EcdsaKeyPair::from_private_key_and_public_key( + alg, + s.expose_secret(), + p.as_slice(), + &*rng, + ) + .map_err(|_| FromBytesError::InvalidKey) + .map(|key| Self::EcdsaP256Sha256 { key, rng }) + } + + ( + SecretKeyBytes::EcdsaP384Sha384(s), + PublicKeyBytes::EcdsaP384Sha384(p), + ) => { + let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; + EcdsaKeyPair::from_private_key_and_public_key( + alg, + s.expose_secret(), + p.as_slice(), + &*rng, + ) + .map_err(|_| FromBytesError::InvalidKey) + .map(|key| Self::EcdsaP384Sha384 { key, rng }) + } + + (SecretKeyBytes::Ed25519(s), PublicKeyBytes::Ed25519(p)) => { + Ed25519KeyPair::from_seed_and_public_key( + s.expose_secret(), + p.as_slice(), + ) + .map_err(|_| FromBytesError::InvalidKey) + .map(Self::Ed25519) + } + + (SecretKeyBytes::Ed448(_), PublicKeyBytes::Ed448(_)) => { + Err(FromBytesError::UnsupportedAlgorithm) + } + + // The public and private key types did not match. + _ => Err(FromBytesError::InvalidKey), + } + } +} + +//--- SignRaw + +impl SignRaw for KeyPair { + fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::EcdsaP256Sha256 { .. } => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384 { .. } => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + } + } + + fn raw_public_key(&self) -> PublicKeyBytes { + match self { + Self::RsaSha256 { key, rng: _ } => { + let components: ring::rsa::PublicKeyComponents> = + key.public().into(); + PublicKeyBytes::RsaSha256(RsaPublicKeyBytes { + n: components.n.into(), + e: components.e.into(), + }) + } + + Self::EcdsaP256Sha256 { key, rng: _ } => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKeyBytes::EcdsaP256Sha256(key.try_into().unwrap()) + } + + Self::EcdsaP384Sha384 { key, rng: _ } => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKeyBytes::EcdsaP384Sha384(key.try_into().unwrap()) + } + + Self::Ed25519(key) => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKeyBytes::Ed25519(key.try_into().unwrap()) + } + } + } + + fn sign_raw(&self, data: &[u8]) -> Result { + match self { + Self::RsaSha256 { key, rng } => { + let mut buf = vec![0u8; key.public().modulus_len()]; + let pad = &ring::signature::RSA_PKCS1_SHA256; + key.sign(pad, &**rng, data, &mut buf) + .map(|()| Signature::RsaSha256(buf.into_boxed_slice())) + .map_err(|_| SignError) + } + + Self::EcdsaP256Sha256 { key, rng } => key + .sign(&**rng, data) + .map(|sig| Box::<[u8]>::from(sig.as_ref())) + .map_err(|_| SignError) + .and_then(|buf| { + buf.try_into() + .map(Signature::EcdsaP256Sha256) + .map_err(|_| SignError) + }), + + Self::EcdsaP384Sha384 { key, rng } => key + .sign(&**rng, data) + .map(|sig| Box::<[u8]>::from(sig.as_ref())) + .map_err(|_| SignError) + .and_then(|buf| { + buf.try_into() + .map(Signature::EcdsaP384Sha384) + .map_err(|_| SignError) + }), + + Self::Ed25519(key) => { + let sig = key.sign(data); + let buf: Box<[u8]> = sig.as_ref().into(); + buf.try_into() + .map(Signature::Ed25519) + .map_err(|_| SignError) + } + } + } +} + +//----------- generate() ----------------------------------------------------- + +/// Generate a new key pair for the given algorithm. +/// +/// While this uses Ring internally, the opaque nature of Ring means that it +/// is not possible to export a secret key from [`KeyPair`]. Thus, the bytes +/// of the secret key are returned directly. +pub fn generate( + params: GenerateParams, + rng: &dyn ring::rand::SecureRandom, +) -> Result<(SecretKeyBytes, PublicKeyBytes), GenerateError> { + match params { + GenerateParams::EcdsaP256Sha256 => { + // Generate a key and a PKCS#8 document out of Ring. + let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; + let doc = EcdsaKeyPair::generate_pkcs8(alg, rng)?; + + // Manually parse the PKCS#8 document for the private key. + let sk: Box<[u8]> = Box::from(&doc.as_ref()[36..68]); + let sk: Box<[u8; 32]> = sk.try_into().unwrap(); + let sk = SecretKeyBytes::EcdsaP256Sha256(sk.into()); + + // Manually parse the PKCS#8 document for the public key. + let pk: Box<[u8]> = Box::from(&doc.as_ref()[73..138]); + let pk = pk.try_into().unwrap(); + let pk = PublicKeyBytes::EcdsaP256Sha256(pk); + + Ok((sk, pk)) + } + + GenerateParams::EcdsaP384Sha384 => { + // Generate a key and a PKCS#8 document out of Ring. + let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; + let doc = EcdsaKeyPair::generate_pkcs8(alg, rng)?; + + // Manually parse the PKCS#8 document for the private key. + let sk: Box<[u8]> = Box::from(&doc.as_ref()[35..83]); + let sk: Box<[u8; 48]> = sk.try_into().unwrap(); + let sk = SecretKeyBytes::EcdsaP384Sha384(sk.into()); + + // Manually parse the PKCS#8 document for the public key. + let pk: Box<[u8]> = Box::from(&doc.as_ref()[88..185]); + let pk = pk.try_into().unwrap(); + let pk = PublicKeyBytes::EcdsaP384Sha384(pk); + + Ok((sk, pk)) + } + + GenerateParams::Ed25519 => { + // Generate a key and a PKCS#8 document out of Ring. + let doc = Ed25519KeyPair::generate_pkcs8(rng)?; + + // Manually parse the PKCS#8 document for the private key. + let sk: Box<[u8]> = Box::from(&doc.as_ref()[16..48]); + let sk: Box<[u8; 32]> = sk.try_into().unwrap(); + let sk = SecretKeyBytes::Ed25519(sk.into()); + + // Manually parse the PKCS#8 document for the public key. + let pk: Box<[u8]> = Box::from(&doc.as_ref()[51..83]); + let pk = pk.try_into().unwrap(); + let pk = PublicKeyBytes::Ed25519(pk); + + Ok((sk, pk)) + } + + _ => Err(GenerateError::UnsupportedAlgorithm), + } +} + +//============ Error Types =================================================== + +/// An error in importing a key pair from bytes into Ring. +#[derive(Clone, Debug)] +pub enum FromBytesError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The provided keypair was invalid. + InvalidKey, + + /// The implementation does not allow such weak keys. + WeakKey, +} + +//--- Formatting + +impl fmt::Display for FromBytesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + Self::WeakKey => "key too weak to be supported", }) } } -impl<'a> SigningKey for Key<'a> { - type Octets = Vec; - type Signature = Signature; - type Error = Unspecified; +//--- Error - fn dnskey(&self) -> Result, Self::Error> { - Ok(self.dnskey.clone()) +impl std::error::Error for FromBytesError {} + +//----------- GenerateError -------------------------------------------------- + +/// An error in generating a key pair with Ring. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +impl From for GenerateError { + fn from(_: ring::error::Unspecified) -> Self { + Self::Implementation + } +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for GenerateError {} + +//============ Tests ========================================================= + +#[cfg(test)] +mod tests { + use std::{sync::Arc, vec::Vec}; + + use crate::{ + base::iana::SecAlg, + sign::{GenerateParams, SecretKeyBytes, SignRaw}, + validate::Key, + }; + + use super::KeyPair; + + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 60616), + (SecAlg::ECDSAP256SHA256, 42253), + (SecAlg::ECDSAP384SHA384, 33566), + (SecAlg::ED25519, 56037), + ]; + + const GENERATE_PARAMS: &[GenerateParams] = &[ + GenerateParams::EcdsaP256Sha256, + GenerateParams::EcdsaP384Sha384, + GenerateParams::Ed25519, + ]; + + #[test] + fn public_key() { + let rng = Arc::new(ring::rand::SystemRandom::new()); + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = Key::>::parse_from_bind(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); + + let key = + KeyPair::from_bytes(&gen_key, pub_key, rng.clone()).unwrap(); + + assert_eq!(key.raw_public_key(), *pub_key); + } } - fn ds( - &self, - owner: N, - ) -> Result, Self::Error> { - let mut buf = Vec::new(); - infallible(owner.compose_canonical(&mut buf)); - infallible(self.dnskey.compose_canonical_rdata(&mut buf)); - let digest = - Vec::from(digest::digest(&digest::SHA256, &buf).as_ref()); - Ok(Ds::new( - self.key_tag()?, - self.dnskey.algorithm(), - DigestAlg::SHA256, - digest, - ) - .expect("long digest")) + #[test] + fn generated_roundtrip() { + let rng = Arc::new(ring::rand::SystemRandom::new()); + for params in GENERATE_PARAMS { + let (sk, pk) = super::generate(params.clone(), &*rng).unwrap(); + let key = KeyPair::from_bytes(&sk, &pk, rng.clone()).unwrap(); + assert_eq!(key.raw_public_key(), pk); + } } - fn sign(&self, msg: &[u8]) -> Result { - match self.key { - RingKey::Ecdsa(ref key) => { - key.sign(self.rng, msg).map(Signature::sig) - } - RingKey::Ed25519(ref key) => Ok(Signature::sig(key.sign(msg))), - RingKey::Rsa(ref key, encoding) => { - let mut sig = vec![0; key.public().modulus_len()]; - key.sign(encoding, self.rng, msg, &mut sig)?; - Ok(Signature::vec(sig)) - } - } - } -} - -pub struct Signature(SignatureInner); - -enum SignatureInner { - Sig(RingSignature), - Vec(Vec), -} - -impl Signature { - fn sig(sig: RingSignature) -> Signature { - Signature(SignatureInner::Sig(sig)) - } - - fn vec(vec: Vec) -> Signature { - Signature(SignatureInner::Vec(vec)) - } -} - -impl AsRef<[u8]> for Signature { - fn as_ref(&self) -> &[u8] { - match self.0 { - SignatureInner::Sig(ref sig) => sig.as_ref(), - SignatureInner::Vec(ref vec) => vec.as_slice(), - } - } -} - -#[cfg(feature = "bytes")] -impl From for Bytes { - fn from(sig: Signature) -> Self { - match sig.0 { - SignatureInner::Sig(sig) => Bytes::copy_from_slice(sig.as_ref()), - SignatureInner::Vec(sig) => Bytes::from(sig), + #[test] + fn sign() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + let rng = Arc::new(ring::rand::SystemRandom::new()); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = Key::>::parse_from_bind(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); + + let key = KeyPair::from_bytes(&gen_key, pub_key, rng).unwrap(); + + let _ = key.sign_raw(b"Hello, World!").unwrap(); } } } diff --git a/src/validate.rs b/src/validate.rs index 41b7456e..0f307fb4 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1,23 +1,790 @@ //! DNSSEC validation. //! //! **This module is experimental and likely to change significantly.** -#![cfg(feature = "validate")] -#![cfg_attr(docsrs, doc(cfg(feature = "validate")))] +#![cfg(feature = "unstable-validate")] +#![cfg_attr(docsrs, doc(cfg(feature = "unstable-validate")))] use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{DigestAlg, SecAlg}; +use crate::base::iana::{Class, DigestAlg, SecAlg}; use crate::base::name::Name; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; +use crate::base::scan::{IterScanner, Scanner}; use crate::base::wire::{Compose, Composer}; -use crate::rdata::{Dnskey, Rrsig}; +use crate::base::zonefile_fmt::ZonefileFmt; +use crate::base::Rtype; +use crate::rdata::{Dnskey, Ds, Rrsig}; use bytes::Bytes; use octseq::builder::with_infallible; +use octseq::{EmptyBuilder, FromBuilder}; use ring::{digest, signature}; +use std::boxed::Box; use std::vec::Vec; use std::{error, fmt}; +//----------- Key ------------------------------------------------------------ + +/// A DNSSEC key for a particular zone. +/// +/// # Serialization +/// +/// Keys can be parsed from or written in the conventional format used by the +/// BIND name server. This is a simplified version of the zonefile format. +/// +/// In this format, a public key is a line-oriented text file. Each line is +/// either blank (having only whitespace) or a single DNSKEY record in the +/// presentation format. In either case, the line may end with a comment (an +/// ASCII semicolon followed by arbitrary content until the end of the line). +/// The file must contain a single DNSKEY record line. +/// +/// The DNSKEY record line contains the following fields, separated by ASCII +/// whitespace: +/// +/// - The owner name. This is an absolute name ending with a dot. +/// - Optionally, the class of the record (usually `IN`). +/// - The record type (which must be `DNSKEY`). +/// - The DNSKEY record data, which has the following sub-fields: +/// - The key flags, which describe the key's uses. +/// - The protocol used (expected to be `3`). +/// - The key algorithm (see [`SecAlg`]). +/// - The public key encoded as a Base64 string. +#[derive(Clone)] +pub struct Key { + /// The owner of the key. + owner: Name, + + /// The flags associated with the key. + /// + /// These flags are stored in the DNSKEY record. + flags: u16, + + /// The public key, in bytes. + /// + /// This identifies the key and can be used for signatures. + key: PublicKeyBytes, +} + +//--- Construction + +impl Key { + /// Construct a new DNSSEC key manually. + pub fn new(owner: Name, flags: u16, key: PublicKeyBytes) -> Self { + Self { owner, flags, key } + } +} + +//--- Inspection + +impl Key { + /// The owner name attached to the key. + pub fn owner(&self) -> &Name { + &self.owner + } + + /// The flags attached to the key. + pub fn flags(&self) -> u16 { + self.flags + } + + /// The raw public key. + pub fn raw_public_key(&self) -> &PublicKeyBytes { + &self.key + } + + /// The signing algorithm used. + pub fn algorithm(&self) -> SecAlg { + self.key.algorithm() + } + + /// Whether this is a zone signing key. + /// + /// From [RFC 4034, section 2.1.1]: + /// + /// > Bit 7 of the Flags field is the Zone Key flag. If bit 7 has value + /// > 1, then the DNSKEY record holds a DNS zone key, and the DNSKEY RR's + /// > owner name MUST be the name of a zone. If bit 7 has value 0, then + /// > the DNSKEY record holds some other type of DNS public key and MUST + /// > NOT be used to verify RRSIGs that cover RRsets. + /// + /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 + pub fn is_zone_signing_key(&self) -> bool { + self.flags & (1 << 8) != 0 + } + + /// Whether this key has been revoked. + /// + /// From [RFC 5011, section 3]: + /// + /// > Bit 8 of the DNSKEY Flags field is designated as the 'REVOKE' flag. + /// > If this bit is set to '1', AND the resolver sees an RRSIG(DNSKEY) + /// > signed by the associated key, then the resolver MUST consider this + /// > key permanently invalid for all purposes except for validating the + /// > revocation. + /// + /// [RFC 5011, section 3]: https://datatracker.ietf.org/doc/html/rfc5011#section-3 + pub fn is_revoked(&self) -> bool { + self.flags & (1 << 7) != 0 + } + + /// Whether this is a secure entry point. + /// + /// From [RFC 4034, section 2.1.1]: + /// + /// > Bit 15 of the Flags field is the Secure Entry Point flag, described + /// > in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a + /// > key intended for use as a secure entry point. This flag is only + /// > intended to be a hint to zone signing or debugging software as to + /// > the intended use of this DNSKEY record; validators MUST NOT alter + /// > their behavior during the signature validation process in any way + /// > based on the setting of this bit. This also means that a DNSKEY RR + /// > with the SEP bit set would also need the Zone Key flag set in order + /// > to be able to generate signatures legally. A DNSKEY RR with the SEP + /// > set and the Zone Key flag not set MUST NOT be used to verify RRSIGs + /// > that cover RRsets. + /// + /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 + /// [RFC3757]: https://datatracker.ietf.org/doc/html/rfc3757 + pub fn is_secure_entry_point(&self) -> bool { + self.flags & 1 != 0 + } + + /// The key tag. + pub fn key_tag(&self) -> u16 { + // NOTE: RSA/MD5 uses a different algorithm. + + // NOTE: A u32 can fit the sum of 65537 u16s without overflowing. A + // key can never exceed 64KiB anyway, so we won't even get close to + // the limit. Let's just add into a u32 and normalize it after. + let mut res = 0u32; + + // Add basic DNSKEY fields. + res += self.flags as u32; + res += u16::from_be_bytes([3, self.algorithm().to_int()]) as u32; + + // Add the raw key tag from the public key. + res += self.key.raw_key_tag(); + + // Normalize and return the result. + (res as u16).wrapping_add((res >> 16) as u16) + } + + /// The digest of this key. + pub fn digest( + &self, + algorithm: DigestAlg, + ) -> Result>, DigestError> + where + Octs: AsRef<[u8]>, + { + let mut context = ring::digest::Context::new(match algorithm { + DigestAlg::SHA1 => &ring::digest::SHA1_FOR_LEGACY_USE_ONLY, + DigestAlg::SHA256 => &ring::digest::SHA256, + DigestAlg::SHA384 => &ring::digest::SHA384, + _ => return Err(DigestError::UnsupportedAlgorithm), + }); + + // Add the owner name. + if self + .owner + .as_slice() + .iter() + .any(|&b| b.is_ascii_uppercase()) + { + let mut owner = [0u8; 256]; + owner[..self.owner.len()].copy_from_slice(self.owner.as_slice()); + owner.make_ascii_lowercase(); + context.update(&owner[..self.owner.len()]); + } else { + context.update(self.owner.as_slice()); + } + + // Add basic DNSKEY fields. + context.update(&self.flags.to_be_bytes()); + context.update(&[3, self.algorithm().to_int()]); + + // Add the public key. + self.key.digest(&mut context); + + // Finalize the digest. + let digest = context.finish().as_ref().into(); + Ok(Ds::new(self.key_tag(), self.algorithm(), algorithm, digest) + .unwrap()) + } +} + +//--- Conversion to and from DNSKEYs + +impl> Key { + /// Deserialize a key from DNSKEY record data. + /// + /// # Errors + /// + /// Fails if the DNSKEY uses an unknown protocol or contains an invalid + /// public key (e.g. one of the wrong size for the signature algorithm). + pub fn from_dnskey( + owner: Name, + dnskey: Dnskey, + ) -> Result { + if dnskey.protocol() != 3 { + return Err(FromDnskeyError::UnsupportedProtocol); + } + + let flags = dnskey.flags(); + let algorithm = dnskey.algorithm(); + let key = dnskey.public_key().as_ref(); + let key = PublicKeyBytes::from_dnskey_format(algorithm, key)?; + Ok(Self { owner, flags, key }) + } + + /// Serialize the key into DNSKEY record data. + /// + /// The owner name can be combined with the returned record to serialize a + /// complete DNS record if necessary. + pub fn to_dnskey(&self) -> Dnskey> { + Dnskey::new( + self.flags, + 3, + self.key.algorithm(), + self.key.to_dnskey_format(), + ) + .expect("long public key") + } + + /// Parse a DNSSEC key from the conventional format used by BIND. + /// + /// See the type-level documentation for a description of this format. + pub fn parse_from_bind(data: &str) -> Result + where + Octs: FromBuilder, + Octs::Builder: EmptyBuilder + Composer, + { + /// Find the next non-blank line in the file. + fn next_line(mut data: &str) -> Option<(&str, &str)> { + let mut line; + while !data.is_empty() { + (line, data) = + data.trim_start().split_once('\n').unwrap_or((data, "")); + if !line.is_empty() && !line.starts_with(';') { + // We found a line that does not start with a comment. + line = line + .split_once(';') + .map_or(line, |(line, _)| line) + .trim_end(); + return Some((line, data)); + } + } + + None + } + + // Ensure there is a single DNSKEY record line in the input. + let (line, rest) = + next_line(data).ok_or(ParseDnskeyTextError::Misformatted)?; + if next_line(rest).is_some() { + return Err(ParseDnskeyTextError::Misformatted); + } + + // Parse the entire record. + let mut scanner = IterScanner::new(line.split_ascii_whitespace()); + + let name = scanner + .scan_name() + .map_err(|_| ParseDnskeyTextError::Misformatted)?; + + let _ = Class::scan(&mut scanner) + .map_err(|_| ParseDnskeyTextError::Misformatted)?; + + if Rtype::scan(&mut scanner).map_or(true, |t| t != Rtype::DNSKEY) { + return Err(ParseDnskeyTextError::Misformatted); + } + + let data = Dnskey::scan(&mut scanner) + .map_err(|_| ParseDnskeyTextError::Misformatted)?; + + Self::from_dnskey(name, data) + .map_err(ParseDnskeyTextError::FromDnskey) + } + + /// Serialize this key in the conventional format used by BIND. + /// + /// See the type-level documentation for a description of this format. + pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { + writeln!( + w, + "{} IN DNSKEY {}", + self.owner().fmt_with_dot(), + self.to_dnskey().display_zonefile(false), + ) + } + + /// Display this key in the conventional format used by BIND. + /// + /// See the type-level documentation for a description of this format. + pub fn display_as_bind(&self) -> impl fmt::Display + '_ { + struct Display<'a, Octs>(&'a Key); + impl<'a, Octs: AsRef<[u8]>> fmt::Display for Display<'a, Octs> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.format_as_bind(f) + } + } + Display(self) + } +} + +//--- Comparison + +impl> PartialEq for Key { + fn eq(&self, other: &Self) -> bool { + self.owner() == other.owner() + && self.flags() == other.flags() + && self.raw_public_key() == other.raw_public_key() + } +} + +//--- Debug + +impl> fmt::Debug for Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Key") + .field("owner", self.owner()) + .field("flags", &self.flags()) + .field("raw_public_key", self.raw_public_key()) + .finish() + } +} + +//----------- RsaPublicKeyBytes ---------------------------------------------- + +/// A low-level public key. +#[derive(Clone, Debug)] +pub enum PublicKeyBytes { + /// An RSA/SHA-1 public key. + RsaSha1(RsaPublicKeyBytes), + + /// An RSA/SHA-1 with NSEC3 public key. + RsaSha1Nsec3Sha1(RsaPublicKeyBytes), + + /// An RSA/SHA-256 public key. + RsaSha256(RsaPublicKeyBytes), + + /// An RSA/SHA-512 public key. + RsaSha512(RsaPublicKeyBytes), + + /// An ECDSA P-256/SHA-256 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (32 bytes). + /// - The encoding of the `y` coordinate (32 bytes). + EcdsaP256Sha256(Box<[u8; 65]>), + + /// An ECDSA P-384/SHA-384 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (48 bytes). + /// - The encoding of the `y` coordinate (48 bytes). + EcdsaP384Sha384(Box<[u8; 97]>), + + /// An Ed25519 public key. + /// + /// The public key is a 32-byte encoding of the public point. + Ed25519(Box<[u8; 32]>), + + /// An Ed448 public key. + /// + /// The public key is a 57-byte encoding of the public point. + Ed448(Box<[u8; 57]>), +} + +//--- Inspection + +impl PublicKeyBytes { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha1(_) => SecAlg::RSASHA1, + Self::RsaSha1Nsec3Sha1(_) => SecAlg::RSASHA1_NSEC3_SHA1, + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::RsaSha512(_) => SecAlg::RSASHA512, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// The raw key tag computation for this value. + fn raw_key_tag(&self) -> u32 { + fn compute(data: &[u8]) -> u32 { + data.chunks(2) + .map(|chunk| { + let mut buf = [0u8; 2]; + // A 0 byte is appended for an incomplete chunk. + buf[..chunk.len()].copy_from_slice(chunk); + u16::from_be_bytes(buf) as u32 + }) + .sum() + } + + match self { + Self::RsaSha1(k) + | Self::RsaSha1Nsec3Sha1(k) + | Self::RsaSha256(k) + | Self::RsaSha512(k) => k.raw_key_tag(), + + Self::EcdsaP256Sha256(k) => compute(&k[1..]), + Self::EcdsaP384Sha384(k) => compute(&k[1..]), + Self::Ed25519(k) => compute(&**k), + Self::Ed448(k) => compute(&**k), + } + } + + /// Compute a digest of this public key. + fn digest(&self, context: &mut ring::digest::Context) { + match self { + Self::RsaSha1(k) + | Self::RsaSha1Nsec3Sha1(k) + | Self::RsaSha256(k) + | Self::RsaSha512(k) => k.digest(context), + + Self::EcdsaP256Sha256(k) => context.update(&k[1..]), + Self::EcdsaP384Sha384(k) => context.update(&k[1..]), + Self::Ed25519(k) => context.update(&**k), + Self::Ed448(k) => context.update(&**k), + } + } +} + +//--- Conversion to and from DNSKEYs + +impl PublicKeyBytes { + /// Parse a public key as stored in a DNSKEY record. + pub fn from_dnskey_format( + algorithm: SecAlg, + data: &[u8], + ) -> Result { + match algorithm { + SecAlg::RSASHA1 => { + RsaPublicKeyBytes::from_dnskey_format(data).map(Self::RsaSha1) + } + SecAlg::RSASHA1_NSEC3_SHA1 => { + RsaPublicKeyBytes::from_dnskey_format(data) + .map(Self::RsaSha1Nsec3Sha1) + } + SecAlg::RSASHA256 => RsaPublicKeyBytes::from_dnskey_format(data) + .map(Self::RsaSha256), + SecAlg::RSASHA512 => RsaPublicKeyBytes::from_dnskey_format(data) + .map(Self::RsaSha512), + + SecAlg::ECDSAP256SHA256 => { + let mut key = Box::new([0u8; 65]); + if key.len() == 1 + data.len() { + key[0] = 0x04; + key[1..].copy_from_slice(data); + Ok(Self::EcdsaP256Sha256(key)) + } else { + Err(FromDnskeyError::InvalidKey) + } + } + SecAlg::ECDSAP384SHA384 => { + let mut key = Box::new([0u8; 97]); + if key.len() == 1 + data.len() { + key[0] = 0x04; + key[1..].copy_from_slice(data); + Ok(Self::EcdsaP384Sha384(key)) + } else { + Err(FromDnskeyError::InvalidKey) + } + } + + SecAlg::ED25519 => Box::<[u8]>::from(data) + .try_into() + .map(Self::Ed25519) + .map_err(|_| FromDnskeyError::InvalidKey), + SecAlg::ED448 => Box::<[u8]>::from(data) + .try_into() + .map(Self::Ed448) + .map_err(|_| FromDnskeyError::InvalidKey), + + _ => Err(FromDnskeyError::UnsupportedAlgorithm), + } + } + + /// Serialize this public key as stored in a DNSKEY record. + pub fn to_dnskey_format(&self) -> Box<[u8]> { + match self { + Self::RsaSha1(k) + | Self::RsaSha1Nsec3Sha1(k) + | Self::RsaSha256(k) + | Self::RsaSha512(k) => k.to_dnskey_format(), + + // From my reading of RFC 6605, the marker byte is not included. + Self::EcdsaP256Sha256(k) => k[1..].into(), + Self::EcdsaP384Sha384(k) => k[1..].into(), + + Self::Ed25519(k) => k.as_slice().into(), + Self::Ed448(k) => k.as_slice().into(), + } + } +} + +//--- Comparison + +impl PartialEq for PublicKeyBytes { + fn eq(&self, other: &Self) -> bool { + use ring::constant_time::verify_slices_are_equal; + + match (self, other) { + (Self::RsaSha1(a), Self::RsaSha1(b)) => a == b, + (Self::RsaSha1Nsec3Sha1(a), Self::RsaSha1Nsec3Sha1(b)) => a == b, + (Self::RsaSha256(a), Self::RsaSha256(b)) => a == b, + (Self::RsaSha512(a), Self::RsaSha512(b)) => a == b, + (Self::EcdsaP256Sha256(a), Self::EcdsaP256Sha256(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::EcdsaP384Sha384(a), Self::EcdsaP384Sha384(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::Ed25519(a), Self::Ed25519(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::Ed448(a), Self::Ed448(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + _ => false, + } + } +} + +impl Eq for PublicKeyBytes {} + +//----------- RsaPublicKeyBytes --------------------------------------------------- + +/// A generic RSA public key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +#[derive(Clone, Debug)] +pub struct RsaPublicKeyBytes { + /// The public modulus. + pub n: Box<[u8]>, + + /// The public exponent. + pub e: Box<[u8]>, +} + +//--- Inspection + +impl RsaPublicKeyBytes { + /// The raw key tag computation for this value. + fn raw_key_tag(&self) -> u32 { + let mut res = 0u32; + + // Extended exponent lengths start with '00 (exp_len >> 8)', which is + // just zero for shorter exponents. That doesn't affect the result, + // so let's just do it unconditionally. + res += (self.e.len() >> 8) as u32; + res += u16::from_be_bytes([self.e.len() as u8, self.e[0]]) as u32; + + let mut chunks = self.e[1..].chunks_exact(2); + res += chunks + .by_ref() + .map(|chunk| u16::from_be_bytes(chunk.try_into().unwrap()) as u32) + .sum::(); + + let n = if !chunks.remainder().is_empty() { + res += + u16::from_be_bytes([chunks.remainder()[0], self.n[0]]) as u32; + &self.n[1..] + } else { + &self.n + }; + + res += n + .chunks(2) + .map(|chunk| { + let mut buf = [0u8; 2]; + buf[..chunk.len()].copy_from_slice(chunk); + u16::from_be_bytes(buf) as u32 + }) + .sum::(); + + res + } + + /// Compute a digest of this public key. + fn digest(&self, context: &mut ring::digest::Context) { + // Encode the exponent length. + if let Ok(exp_len) = u8::try_from(self.e.len()) { + context.update(&[exp_len]); + } else if let Ok(exp_len) = u16::try_from(self.e.len()) { + context.update(&[0u8, (exp_len >> 8) as u8, exp_len as u8]); + } else { + unreachable!("RSA exponents are (much) shorter than 64KiB") + } + + context.update(&self.e); + context.update(&self.n); + } +} + +//--- Conversion to and from DNSKEYs + +impl RsaPublicKeyBytes { + /// Parse an RSA public key as stored in a DNSKEY record. + pub fn from_dnskey_format(data: &[u8]) -> Result { + if data.len() < 3 { + return Err(FromDnskeyError::InvalidKey); + } + + // The exponent length is encoded as 1 or 3 bytes. + let (exp_len, off) = if data[0] != 0 { + (data[0] as usize, 1) + } else if data[1..3] != [0, 0] { + // NOTE: Even though this is the extended encoding of the length, + // a user could choose to put a length less than 256 over here. + let exp_len = u16::from_be_bytes(data[1..3].try_into().unwrap()); + (exp_len as usize, 3) + } else { + // The extended encoding of the length just held a zero value. + return Err(FromDnskeyError::InvalidKey); + }; + + // NOTE: off <= 3 so is safe to index up to. + let e = data[off..] + .get(..exp_len) + .ok_or(FromDnskeyError::InvalidKey)? + .into(); + + // NOTE: The previous statement indexed up to 'exp_len'. + let n = data[off + exp_len..].into(); + + Ok(Self { n, e }) + } + + /// Serialize this public key as stored in a DNSKEY record. + pub fn to_dnskey_format(&self) -> Box<[u8]> { + let mut key = Vec::new(); + + // Encode the exponent length. + if let Ok(exp_len) = u8::try_from(self.e.len()) { + key.reserve_exact(1 + self.e.len() + self.n.len()); + key.push(exp_len); + } else if let Ok(exp_len) = u16::try_from(self.e.len()) { + key.reserve_exact(3 + self.e.len() + self.n.len()); + key.push(0u8); + key.extend(&exp_len.to_be_bytes()); + } else { + unreachable!("RSA exponents are (much) shorter than 64KiB") + } + + key.extend(&*self.e); + key.extend(&*self.n); + key.into_boxed_slice() + } +} + +//--- Comparison + +impl PartialEq for RsaPublicKeyBytes { + fn eq(&self, other: &Self) -> bool { + use ring::constant_time::verify_slices_are_equal; + + verify_slices_are_equal(&self.n, &other.n).is_ok() + && verify_slices_are_equal(&self.e, &other.e).is_ok() + } +} + +impl Eq for RsaPublicKeyBytes {} + +//----------- Signature ------------------------------------------------------ + +/// A cryptographic signature. +/// +/// The format of the signature varies depending on the underlying algorithm: +/// +/// - RSA: the signature is a single integer `s`, which is less than the key's +/// public modulus `n`. `s` is encoded as bytes and ordered from most +/// significant to least significant digits. It must be at least 64 bytes +/// long and at most 512 bytes long. Leading zero bytes can be inserted for +/// padding. +/// +/// See [RFC 3110](https://datatracker.ietf.org/doc/html/rfc3110). +/// +/// - ECDSA: the signature has a fixed length (64 bytes for P-256, 96 for +/// P-384). It is the concatenation of two fixed-length integers (`r` and +/// `s`, each of equal size). +/// +/// See [RFC 6605](https://datatracker.ietf.org/doc/html/rfc6605) and [SEC 1 +/// v2.0](https://www.secg.org/sec1-v2.pdf). +/// +/// - EdDSA: the signature has a fixed length (64 bytes for ED25519, 114 bytes +/// for ED448). It is the concatenation of two curve points (`R` and `S`) +/// that are encoded into bytes. +/// +/// Signatures are too big to pass by value, so they are placed on the heap. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Signature { + RsaSha1(Box<[u8]>), + RsaSha1Nsec3Sha1(Box<[u8]>), + RsaSha256(Box<[u8]>), + RsaSha512(Box<[u8]>), + EcdsaP256Sha256(Box<[u8; 64]>), + EcdsaP384Sha384(Box<[u8; 96]>), + Ed25519(Box<[u8; 64]>), + Ed448(Box<[u8; 114]>), +} + +impl Signature { + /// The algorithm used to make the signature. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha1(_) => SecAlg::RSASHA1, + Self::RsaSha1Nsec3Sha1(_) => SecAlg::RSASHA1_NSEC3_SHA1, + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::RsaSha512(_) => SecAlg::RSASHA512, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + match self { + Self::RsaSha1(s) + | Self::RsaSha1Nsec3Sha1(s) + | Self::RsaSha256(s) + | Self::RsaSha512(s) => s, + Self::EcdsaP256Sha256(s) => &**s, + Self::EcdsaP384Sha384(s) => &**s, + Self::Ed25519(s) => &**s, + Self::Ed448(s) => &**s, + } + } +} + +impl From for Box<[u8]> { + fn from(value: Signature) -> Self { + match value { + Signature::RsaSha1(s) + | Signature::RsaSha1Nsec3Sha1(s) + | Signature::RsaSha256(s) + | Signature::RsaSha512(s) => s, + Signature::EcdsaP256Sha256(s) => s as _, + Signature::EcdsaP384Sha384(s) => s as _, + Signature::Ed25519(s) => s as _, + Signature::Ed448(s) => s as _, + } + } +} + //------------ Dnskey -------------------------------------------------------- /// Extensions for DNSKEY record type. @@ -377,6 +1144,71 @@ fn rsa_exponent_modulus( //============ Error Types =================================================== +//----------- DigestError ---------------------------------------------------- + +/// An error when computing a digest. +#[derive(Clone, Debug)] +pub enum DigestError { + UnsupportedAlgorithm, +} + +//--- Display, Error + +impl fmt::Display for DigestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +impl error::Error for DigestError {} + +//----------- FromDnskeyError ------------------------------------------------ + +/// An error in reading a DNSKEY record. +#[derive(Clone, Debug)] +pub enum FromDnskeyError { + UnsupportedAlgorithm, + UnsupportedProtocol, + InvalidKey, +} + +//--- Display, Error + +impl fmt::Display for FromDnskeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "unsupported algorithm", + Self::UnsupportedProtocol => "unsupported protocol", + Self::InvalidKey => "malformed key", + }) + } +} + +impl error::Error for FromDnskeyError {} + +//----------- ParseDnskeyTextError ------------------------------------------- + +#[derive(Clone, Debug)] +pub enum ParseDnskeyTextError { + Misformatted, + FromDnskey(FromDnskeyError), +} + +//--- Display, Error + +impl fmt::Display for ParseDnskeyTextError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Misformatted => "misformatted DNSKEY record", + Self::FromDnskey(e) => return e.fmt(f), + }) + } +} + +impl error::Error for ParseDnskeyTextError {} + //------------ AlgorithmError ------------------------------------------------ /// An algorithm error during verification. @@ -387,17 +1219,15 @@ pub enum AlgorithmError { InvalidData, } -//--- Display and Error +//--- Display, Error impl fmt::Display for AlgorithmError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - AlgorithmError::Unsupported => { - f.write_str("unsupported algorithm") - } - AlgorithmError::BadSig => f.write_str("bad signature"), - AlgorithmError::InvalidData => f.write_str("invalid data"), - } + f.write_str(match self { + AlgorithmError::Unsupported => "unsupported algorithm", + AlgorithmError::BadSig => "bad signature", + AlgorithmError::InvalidData => "invalid data", + }) } } @@ -416,12 +1246,23 @@ mod test { use crate::utils::base64; use bytes::Bytes; use std::str::FromStr; + use std::string::ToString; type Name = crate::base::name::Name>; type Ds = crate::rdata::Ds>; type Dnskey = crate::rdata::Dnskey>; type Rrsig = crate::rdata::Rrsig, Name>; + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA1, 439), + (SecAlg::RSASHA1_NSEC3_SHA1, 22204), + (SecAlg::RSASHA256, 60616), + (SecAlg::ECDSAP256SHA256, 42253), + (SecAlg::ECDSAP384SHA384, 33566), + (SecAlg::ED25519, 56037), + (SecAlg::ED448, 7379), + ]; + // Returns current root KSK/ZSK for testing (2048b) fn root_pubkey() -> (Dnskey, Dnskey) { let ksk = base64::decode::>( @@ -466,6 +1307,85 @@ mod test { ) } + #[test] + fn parse_from_bind() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let _ = Key::>::parse_from_bind(&data).unwrap(); + } + } + + #[test] + fn key_tag() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = Key::>::parse_from_bind(&data).unwrap(); + assert_eq!(key.to_dnskey().key_tag(), key_tag); + assert_eq!(key.key_tag(), key_tag); + } + } + + #[test] + fn digest() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = Key::>::parse_from_bind(&data).unwrap(); + + // Scan the DS record from the file. + let path = format!("test-data/dnssec-keys/K{}.ds", name); + let data = std::fs::read_to_string(path).unwrap(); + let mut scanner = IterScanner::new(data.split_ascii_whitespace()); + let _ = scanner.scan_name().unwrap(); + let _ = Class::scan(&mut scanner).unwrap(); + assert_eq!(Rtype::scan(&mut scanner).unwrap(), Rtype::DS); + let ds = Ds::scan(&mut scanner).unwrap(); + + assert_eq!(key.digest(ds.digest_type()).unwrap(), ds); + } + } + + #[test] + fn dnskey_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = Key::>::parse_from_bind(&data).unwrap(); + let dnskey = key.to_dnskey().convert(); + let same = Key::from_dnskey(key.owner().clone(), dnskey).unwrap(); + assert_eq!(key, same); + } + } + + #[test] + fn bind_format_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = Key::>::parse_from_bind(&data).unwrap(); + let bind_fmt_key = key.display_as_bind().to_string(); + let same = Key::parse_from_bind(&bind_fmt_key).unwrap(); + assert_eq!(key, same); + } + } + #[test] fn dnskey_digest() { let (dnskey, _) = root_pubkey(); diff --git a/test-data/dnssec-keys/Ktest.+005+00439.ds b/test-data/dnssec-keys/Ktest.+005+00439.ds new file mode 100644 index 00000000..54313710 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+005+00439.ds @@ -0,0 +1 @@ +test. IN DS 439 5 1 3d54b51d59c71418104ec48bacb3d1a01b8eaa30 diff --git a/test-data/dnssec-keys/Ktest.+005+00439.key b/test-data/dnssec-keys/Ktest.+005+00439.key new file mode 100644 index 00000000..35999a0a --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+005+00439.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 5 AwEAAb5nA65uEYX1bRYwT53jRQqAk/mLbi3SlN3xxkdtn7rTkKgEdiBPIF8+0OVyS3x/OCLPTrto6ojUI5etA1VDZPiTLvuq6rIhn3oNyc5o9Kzl4RX4XptLTrt7ldRcpIjgcgqMJoERUWLQqxoXCfRqClxO2Erk0UZhe3GteCMSEfoGBU5MdPzrrEE6GMxEAKFHabjupQ4GazxfWO7+D38lsmUNJwgCg/B14CIcvTS6cHKFmKJKYEEmAj/kx+LnZd9bmeyagFz8CcgcI/NUiSDgdgx/OeCdSc39OHCp9a0NSJuywbbIxpLPw8cIvgZ8OnHuGjrNTROuyYXVxQM1xe914DM= ;{id = 439 (ksk), size = 2048b} diff --git a/test-data/dnssec-keys/Ktest.+005+00439.private b/test-data/dnssec-keys/Ktest.+005+00439.private new file mode 100644 index 00000000..1d8d11ce --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+005+00439.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 5 (RSASHA1) +Modulus: vmcDrm4RhfVtFjBPneNFCoCT+YtuLdKU3fHGR22futOQqAR2IE8gXz7Q5XJLfH84Is9Ou2jqiNQjl60DVUNk+JMu+6rqsiGfeg3Jzmj0rOXhFfhem0tOu3uV1FykiOByCowmgRFRYtCrGhcJ9GoKXE7YSuTRRmF7ca14IxIR+gYFTkx0/OusQToYzEQAoUdpuO6lDgZrPF9Y7v4PfyWyZQ0nCAKD8HXgIhy9NLpwcoWYokpgQSYCP+TH4udl31uZ7JqAXPwJyBwj81SJIOB2DH854J1Jzf04cKn1rQ1Im7LBtsjGks/Dxwi+Bnw6ce4aOs1NE67JhdXFAzXF73XgMw== +PublicExponent: AQAB +PrivateExponent: CSEarcAR+ltUhK4s/cQKPmurLK7rydSsAKGkFoQCFvd9RcvDojRJDWgPT2vAhNKmGBKFPY/VQa7yRJvYv2YrhDkCarISQ2zrSZ3kTDpUvlQzYQCiAKOGveSPauRE8K8vqKPPANHva2PX9bifEzy2YctXVu1Lv3/TEcCgibCcc2FwrKqzwHZ/AvMeMQD7UjetkpFELqYRHkdFQt+8vFDTmXNhhtm2O5xgYymsaaLW7mOLyR7oo25Uk93ouZx3Ibo9yNHdeJG6S6wFeWQaLGKA78tJK10gaUwiHIdEYh4qQ+pSsjztk6A2ObaWmlbt5Ve9qN1WW+KVizATJIQUQvhocQ== +Prime1: 42WKyzrGcBkhZz8xTvNWzlkhvb6aHgryXlgMP2E1GxRgZDApj6XqFzDHRbC/QaRvZi9skuoEz148xH6Hs2oJQ3I/2+dX/7YmnwPZyxHCx2LUlQ+AqEXXWNGCXQ5I6EvDDFeLSqb7m4sZhnnMaTOpyrmYqFzkxZkWrNiSHJjq5us= +Prime2: 1lo1/h5mxzarMFwfrOI+ErR8bvYrAp8hr33MV58MUwWy2IyUIlJRPJVg6DAaT87jwQuJEVarqq2IB48TI1SKglR5CJNcRuTviHWVViXDY7AVnUvHWiiKncTKDQG7vI4Ffft46qVEdaKLjkPBsapuibt0ocpKszVdmr0usP31qdk= +Exponent1: VIQbD+nqcyOD/MHJ69QZgVwzZDiBQ4VCC7qh4rSYblYmdVZJPDCoTrI8fjRxAU7CcLJTok8ENqaJ42Y7vX09sCm4flz/ofTradKekhEp2b1r0XMPmHtMzKAh2cBDbMMr3Vx0Uuy5O1h5xjdit/8Rrl1I1dqg1KhPezKLK8HSHL0= +Exponent2: QqGALyIcKMjhpgK9Bey+Bup707JJ5GK7AeZE4ufZ2OTol0/7rD+SaRa2LPbm9vAE9Dk1vmIGsuOGaXMcK9tXwvOnO/cytAbuPqjuZv0OI6rUzTSFH42CqVBGzow/Y3lyU5scFzSQd1CzuOFvEF8+RSo0MybC2bo5AqTUIsiO2OE= +Coefficient: wOxhD2sDrZhzWq99qjyaYSZxQrPhJWkLR8LhnZEmPlQwfExz939Qw1TkmBpYcr67sN8UTqY93N7mES2LOJrkE/RzstzaKQS2We8mypovFOwcZu3GfJSsRYJRhsW5dEIiLAVw8a/bnC+K0m2Ahiy8v3GwQVo0u1KZ6oSHmG8IWng= diff --git a/test-data/dnssec-keys/Ktest.+007+22204.ds b/test-data/dnssec-keys/Ktest.+007+22204.ds new file mode 100644 index 00000000..91357509 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+007+22204.ds @@ -0,0 +1 @@ +test. IN DS 22204 7 1 0783210826bc4a4ab0d4b329458f216bf787a00c diff --git a/test-data/dnssec-keys/Ktest.+007+22204.key b/test-data/dnssec-keys/Ktest.+007+22204.key new file mode 100644 index 00000000..26bf24bf --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+007+22204.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 7 AwEAAcOFirT7uFYwPyEhyio+mb/9yMQH6ENYEOboEX2c0WIPBFr1s34rZ3SWEWsTvxLOMKr3drzSZtcpCQ6vEyPpQpGo1cpWlVSZ7QB73iWw21rZkz/r4MykyloPoJ8ghr4SRSfJx6CjAb+Fhz3bUF4YWofJEshuZMbxLnOEi2hR9T2zTPRjYltA1sfhU478ixh6ddNym+kCIBEhoFIFyKYb5VznOoWcR/mOexQMfUdNqKoIwnhCX8Sg2dKYdgeDDPsZH3AaWp8BY3aqiqOEacSO2XI+7Pdr0rVfszJfcCsf4g+R/7oBt6dtO9WS+0YqVN0J8WQ/9HmWFeCJgY2Rs4c9eDk= ;{id = 22204 (ksk), size = 2048b} diff --git a/test-data/dnssec-keys/Ktest.+007+22204.private b/test-data/dnssec-keys/Ktest.+007+22204.private new file mode 100644 index 00000000..ecb576d4 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+007+22204.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 7 (RSASHA1_NSEC3) +Modulus: w4WKtPu4VjA/ISHKKj6Zv/3IxAfoQ1gQ5ugRfZzRYg8EWvWzfitndJYRaxO/Es4wqvd2vNJm1ykJDq8TI+lCkajVylaVVJntAHveJbDbWtmTP+vgzKTKWg+gnyCGvhJFJ8nHoKMBv4WHPdtQXhhah8kSyG5kxvEuc4SLaFH1PbNM9GNiW0DWx+FTjvyLGHp103Kb6QIgESGgUgXIphvlXOc6hZxH+Y57FAx9R02oqgjCeEJfxKDZ0ph2B4MM+xkfcBpanwFjdqqKo4RpxI7Zcj7s92vStV+zMl9wKx/iD5H/ugG3p2071ZL7RipU3QnxZD/0eZYV4ImBjZGzhz14OQ== +PublicExponent: AQAB +PrivateExponent: VaLpgGCaOgHSvK/AjOUzUVCWPSobdFefu4sckhB78v+R0Ec6cUIQg5NxGJ2i/FkcHt3Zf1WGXqnmAizzbLCvi/3PedqXeGEc2a/nOknuoamXYZFuOiPZTz32A4xrB9gXuxgZXAXZb6nL9O9YkYYILN4IYIpdkHc1ebotlykCiZ14YjS7sFiKNwxk4Pk5HC9qwQlRujO2LZN6Gp5Pqj3i8h/d9/xgCV+IJGwUiy8y0czEJH3f+k76IaM4ZQZiieS/3vXmytHieAVGIZBH5yztgy+p+GJgVXPEb/7WESC38WSn6GwqthcBZXrSOjhqP2PfFuDDfEhglTNSBqhONzE28w== +Prime1: 9trbMq0VgNtsJuyM5CMQa/feEidp51a1POok8pPAZ6SUpno+oNzITCrSga7i08HzBoW22k9jNmIJmpwXDeDoX2TICgDEyzIqzBH+V1zCE1dI8fv9w/hF9mt/qoZ0PN/Jh4Zcu/AHtmRaHAO6lBFblS6EZxdX4lTeVj8toGxR0ms= +Prime2: ysPYyIh9vwN5rKNPKnrjPtMshjFv6CEnXeFDhVvxcutudgayyu0+Gu8g54WjJ/tpEsDENjhi1Da21pn5RxpgCbe/qE+2Z7CGsw+FI+UcOgx8EEm1aGSenC+7AVACarPtU6zr5/kcPiqCm6zPatLJvXRfbQAa/hHdl5Xg28HX8Os= +Exponent1: Da4zV6uf9XQzmjSh2kLXNiSWegsVI2z6vlV7lrX5g8TrOA6uSdvyfcYhxG4cw/+LqGDgsViU9v6X6amc3XgJaL/9FhDU1y4AkS6uGclaOBguQrrkZWfs+KsceCbbakQ8tvYLTZ8PzlvhYowSWwJbQPlC/TOd+z0Y1U7LCIj4P+E= +Exponent2: LnOrqFVMqYP8TgajzlGU2gG7A4sz3fQqdqFyvIyRxggVqEhkkYTEY5tA6Il/FVvNeJRc3ycPzRozzPo9V4K9WbyU1dRdL2gLk94MXGrSiqHtkjWwr5fNlm6A4w4XX6aUykSlTuGNDNjkTxHJ+ukLerG8YtZRWL9zCpU1jGLeO70= +Coefficient: quDhRGQcA/iLpbDJym2ErykV+wsflci0KZIf7/rtCnsDJZSVYQlB/UPY2S5ne+zwuY8/fNYGIVMYN1sV8OPF3AIpTOtte5pc+1V+4rbuQEQhQw9uIvX4205GEc2sjJ637CT46FDP/lnPL7TdvV6NdOuLyDDImbaMqyLtMSJ5IEs= diff --git a/test-data/dnssec-keys/Ktest.+008+60616.ds b/test-data/dnssec-keys/Ktest.+008+60616.ds new file mode 100644 index 00000000..65444f94 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+60616.ds @@ -0,0 +1 @@ +test. IN DS 60616 8 2 6b91f7b7134cf916d909e2905b5707e3ea6c86842339f09d87c858d7ccd620b3 diff --git a/test-data/dnssec-keys/Ktest.+008+60616.key b/test-data/dnssec-keys/Ktest.+008+60616.key new file mode 100644 index 00000000..fa6c03d8 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+60616.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 8 AwEAAdaxEmT1eAAnXMGDjYfivh6ax6BOESlNZY85BlVWkCOYV6jf5GcSgweqcCowFW2HtHKiE/FACwG5Wfq/xCDhLHYg4PQIvd5UcrDzj+WBEFe7pVhUjZrMsMRAVy2W4jliat6IrJv+CdycErp4cLxmqfNECIP7i9vI8onruvBe1YWebJN38TxdGCteg5waI27DNaQsXldxZoCfSY7Fkhj7BJ4XxHDeWzE876LmSMkkYFWqEQwesD280piL+4tmySMPxhVC1EUguQyn/Lc9FbEd3h1RyaO8hg8ub/70espLVElE9ImOibaY+gj9jK7HFD/mqdxYdFfr3yiQsGOt2ui4jGM= ;{id = 60616 (ksk), size = 2048b} diff --git a/test-data/dnssec-keys/Ktest.+008+60616.private b/test-data/dnssec-keys/Ktest.+008+60616.private new file mode 100644 index 00000000..8df7cdc2 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+60616.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 8 (RSASHA256) +Modulus: 1rESZPV4ACdcwYONh+K+HprHoE4RKU1ljzkGVVaQI5hXqN/kZxKDB6pwKjAVbYe0cqIT8UALAblZ+r/EIOEsdiDg9Ai93lRysPOP5YEQV7ulWFSNmsywxEBXLZbiOWJq3oism/4J3JwSunhwvGap80QIg/uL28jyieu68F7VhZ5sk3fxPF0YK16DnBojbsM1pCxeV3FmgJ9JjsWSGPsEnhfEcN5bMTzvouZIySRgVaoRDB6wPbzSmIv7i2bJIw/GFULURSC5DKf8tz0VsR3eHVHJo7yGDy5v/vR6yktUSUT0iY6Jtpj6CP2MrscUP+ap3Fh0V+vfKJCwY63a6LiMYw== +PublicExponent: AQAB +PrivateExponent: EBBYZ6ofnCYAgGY/J8S6easWdr3V9jjZTtnIdIgxPsiTqTTKGpWTAkwpb66rW8evTnMmz4KoOtfLOMIygvdLjrHabcgIVONitYTJO+CSqs3aiv0V9K2OKGZcCjLjoxbkbNmIeMo4TgPLjvJGFS1lV/4Q2Qya+WCpbSfF6V20gkvQ46dtdRaswFOeav0WIm8LdudWDlYei89EIL243JlDErRmcrh6ZrxIg2TMT+mYJCoM6zfhFkbZuQyagn0Fguymp3Kc31SFqdReF9Q/IIQKwNiW14gdxCEHxq+y7xajCF0bhRZAY/hVyRr4qpx2ZRNMdg5qR2a8IilhH2+YXkHBUQ== +Prime1: 7fuvTpTPTHAQV3nQEW6WLf9xrf0G6ka5E2Lvn+jaawk60VZHoVybpURd0Dq586ZinQpJ2ovEfCd9Os8vn31BNrtulz8mfmKz1rObbdKvo0XRSExcLFx2ZG35Bdo/6H8Ri5e/9gx0m0yJeKspNW20uJX9ndk8Lsm5J9d+8SvcZis= +Prime2: 5vH6ly1VSF1DafdVGMKiHY4icP4OAAPJ/Sl+ihcYzbguhZ82fJ3mZeYLDZWSozwnvhK9PTqGwVRhLJH875AUrU/YA+nEBb5dVHMgGb4Afx2PzOlhgDIhEiRD0QW/9bwq45nITfnFMbYzkE2e08KZ/tjiusQIRZAQCkEBEbNITqk= +Exponent1: pKvW7iUCG/4fEKh1VNqUiFeNLbs7obg2MDfxX1EccZv9WwS8o+cUvBLGZ2N7cCDdc5S+7b5wwwgAG0Vpyo49JcYkC/vigumBTzsQfbmfVvbkjYZo8Tk5otyFx4rxVcs3NMRYS8Tqmtsm9Jxa82Fp/5+p0iOTBT0IJY1zhSW4Z+k= +Exponent2: kvemyxIUVarUPdkiFFG4LSrIjDOA4U2H+02us14jcLcnE+3QFNm/R1Vv70MiQDMF75WpTA+0tc9mz6BP4HxGTEylYUggcK9GYXmqEfeyBTLg0jwqyhQcq5jcd2Y7VLxcZt70c3rhnNMgWVKsIoKS0XVgRA6AXRRiwMPBVGxNNZE= +Coefficient: HsJ5e503CSA3lF3sPrKuL4EuT1Qv0IMHRSd5cZyJj6fCvLYzXi+NtlUX+GMHKuzSm64t6Jrw+FN2I1XTn0QvnpMQqwgou/G79I3dy3a82B+I2qBXgPFqpb/Zj6Eno+aQ+jxD4i6C2b7GhpAxpENwBLIPoIhyJSmWl1o2DDo2irs= diff --git a/test-data/dnssec-keys/Ktest.+013+42253.ds b/test-data/dnssec-keys/Ktest.+013+42253.ds new file mode 100644 index 00000000..8d52a130 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+42253.ds @@ -0,0 +1 @@ +test. IN DS 42253 13 2 b55c30248246756635ee8eb9ff03a9492df46257f4f6537ea85e579b501765e6 diff --git a/test-data/dnssec-keys/Ktest.+013+42253.key b/test-data/dnssec-keys/Ktest.+013+42253.key new file mode 100644 index 00000000..c9d6127e --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+42253.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 13 /5DQ8gQAUp0yITNeE6p0rKQPblVGKOPAdPKxWLQ/FOrkcax3S7qJZh6Z9ayn+EewnpQcmdexlOvxsMf5q8ppCw== ;{id = 42253 (ksk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+013+42253.private b/test-data/dnssec-keys/Ktest.+013+42253.private new file mode 100644 index 00000000..7b26e96a --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+42253.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: uKp4Xz2aB3/LfLGADBjNYFvAZbDHBCO+uJdL+GFCVOY= diff --git a/test-data/dnssec-keys/Ktest.+014+33566.ds b/test-data/dnssec-keys/Ktest.+014+33566.ds new file mode 100644 index 00000000..7e3165c6 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+33566.ds @@ -0,0 +1 @@ +test. IN DS 33566 14 4 d27e8964b63e8f3db4001834d03f1034669e5d39500b06863cc9f38cd649131421bb78b0b08f0ec61a8c8caf0cf09a19 diff --git a/test-data/dnssec-keys/Ktest.+014+33566.key b/test-data/dnssec-keys/Ktest.+014+33566.key new file mode 100644 index 00000000..dd967bcc --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+33566.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 14 mce1CBcESReUP0iQYCnnhoWrVYe86PnFHIkKkr7qmO5q7AwAENchMaBPzaPOOuwx8Z8AcqIjXLOGL13RDT1lvLLkH7IJMIPHRwiXiFoj0KXBugvKLmMT3a0Nc8s8Uau9 ;{id = 33566 (ksk), size = 384b} diff --git a/test-data/dnssec-keys/Ktest.+014+33566.private b/test-data/dnssec-keys/Ktest.+014+33566.private new file mode 100644 index 00000000..276b9d31 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+33566.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 14 (ECDSAP384SHA384) +PrivateKey: 3e1YdfRwn8YOX3Ai84BWVLl3/SphcQIeCkvQnzszKqR3U2xmq/G5HtiGTnBZ1WSW diff --git a/test-data/dnssec-keys/Ktest.+015+56037.ds b/test-data/dnssec-keys/Ktest.+015+56037.ds new file mode 100644 index 00000000..fb802353 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+56037.ds @@ -0,0 +1 @@ +test. IN DS 56037 15 2 665c358b671a9ed5310667b2bacfb526ace344f59d085c8331c532e6a7024f75 diff --git a/test-data/dnssec-keys/Ktest.+015+56037.key b/test-data/dnssec-keys/Ktest.+015+56037.key new file mode 100644 index 00000000..38dc516a --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+56037.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 15 ml9GKFR/doUuYnnQSPi6uiqvHV4VUGOjD4gmpc5dudc= ;{id = 56037 (ksk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+015+56037.private b/test-data/dnssec-keys/Ktest.+015+56037.private new file mode 100644 index 00000000..52c5034a --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+56037.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 15 (ED25519) +PrivateKey: Xg9BfVadQ07eubbyryukpn6lYr9BwDHBLSUOpaGLdrc= diff --git a/test-data/dnssec-keys/Ktest.+016+07379.ds b/test-data/dnssec-keys/Ktest.+016+07379.ds new file mode 100644 index 00000000..a1ca41c4 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+07379.ds @@ -0,0 +1 @@ +test. IN DS 7379 16 2 0ec6db96a33efb0c80c9a90e34e80d32506883d0ed245eefd7bfa4d6e13927c9 diff --git a/test-data/dnssec-keys/Ktest.+016+07379.key b/test-data/dnssec-keys/Ktest.+016+07379.key new file mode 100644 index 00000000..a7eade4f --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+07379.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 16 9tIYxOhfSE0dS7m9mVxjgMeWJ5arrusV9VSvxYrbJVhucOm6I35HpHi4Eau5P06vpHaMdbp3aFOA ;{id = 7379 (ksk), size = 456b} diff --git a/test-data/dnssec-keys/Ktest.+016+07379.private b/test-data/dnssec-keys/Ktest.+016+07379.private new file mode 100644 index 00000000..9d837bcc --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+07379.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 16 (ED448) +PrivateKey: /hmHKRERsvW761FDTmGlCBJNmy1H8pbsU2LeV1NP2wb0xM286RFIyUMAwRmkFqPVZwwfQluIBXqe