From 8e8d616ee2a4b6909c5ee3436f3a3c6fff2d2ec9 Mon Sep 17 00:00:00 2001 From: arya dradjica <131501786+bal-e@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:57:55 +0100 Subject: [PATCH] Loading, storing, and generating DNSSEC keys (#406) * [sign] Define 'KeyPair' and impl key export A private key converted into a 'KeyPair' can be exported in the conventional DNS format. This is an important step in implementing 'ldns-keygen' using 'domain'. It is up to the implementation modules to provide conversion to and from 'KeyPair'; some impls (e.g. for HSMs) won't support it at all. * [sign] Define trait 'Sign' 'Sign' is a more generic version of 'sign::key::SigningKey' that does not provide public key information. It does not try to abstract over all the functionality of a keypair, since that can depend on the underlying cryptographic implementation. * [sign] Implement parsing from the DNS format There are probably lots of bugs in this implementation, I'll add some tests soon. * [sign] Provide some error information Also fixes 'cargo clippy' issues, particularly with the MSRV. * [sign] Move 'KeyPair' to 'generic::SecretKey' I'm going to add a corresponding 'PublicKey' type, at which point it becomes important to differentiate from the generic representations and actual cryptographic implementations. * [sign/generic] Add 'PublicKey' * [sign] Rewrite the 'ring' module to use the 'Sign' trait Key generation, for now, will only be provided by the OpenSSL backend (coming soon). However, generic keys (for RSA/SHA-256 or Ed25519) can be imported into the Ring backend and used freely. * Implement DNSSEC signing with OpenSSL The OpenSSL backend supports import from and export to generic secret keys, making the formatting and parsing machinery for them usable. The next step is to implement generation of keys. * [sign/openssl] Implement key generation * [sign/openssl] Test key generation and import/export * [sign/openssl] Add support for ECDSA * [sign/openssl] satisfy clippy * [sign/openssl] Implement the 'Sign' trait * Install OpenSSL in CI builds * Ensure 'openssl' dep supports 3.x.x * [workflows/ci] Use 'vcpkg' instead of vendoring OpenSSL * Ensure 'openssl' dep exposes necessary interfaces * [workflows/ci] Record location of 'vcpkg' * [workflows/ci] Use a YAML def for 'VCPKG_ROOT' * [workflows/ci] Fix a vcpkg triplet to use * Upgrade openssl to 0.10.57 for bitflags 2.x * [workflows/ci] Use dynamic linking for vcpkg openssl * [workflows/ci] Correctly annotate 'vcpkg' * [sign/openssl] Implement exporting public keys * [sign/ring] Implement exporting public keys * [sign/generic] Test (de)serialization for generic secret keys There were bugs in the Base64 encoding/decoding that are not worth trying to debug; there's a perfectly usable Base64 implementation in the crate already. * [sign] Thoroughly test import/export in both backends I had to swap out the RSA key since 'ring' found it to be too small. * [sign] Remove debugging code and satisfy clippy * [sign] Account for CR LF in tests * [sign/openssl] Fix bugs in the signing procedure - RSA signatures were being made with an unspecified padding scheme. - ECDSA signatures were being output in ASN.1 DER format, instead of the fixed-size format required by DNSSEC (and output by 'ring'). - Tests for signature failures are now added for both backends. * Refactor the 'sign' module Most functions have been renamed. The public key types have been moved to the 'validate' module (which 'sign' now depends on), and they have been outfitted with conversions (e.g. to and from DNSKEY records). Importing a generic key into an OpenSSL or Ring key now requires the public key to also be available. In both implementations, the pair are checked for consistency -- this ensures that both are uncorrupted and that keys have not been mixed up. This also allows the Ring backend to support ECDSA keys (although key generation is still difficult). The 'PublicKey' and 'PrivateKey' enums now store their array data in 'Box'. This has two benefits: it is easier to securely manage memory on the heap (since the compiler will not copy it around the stack); and the smaller sizes of the types is beneficial (although negligibly) to performance. * Move 'sign' and 'validate' to unstable feature gates * [workflows/ci] Document the vcpkg env vars * Rename public/secret key interfaces to '*Raw*' This makes space for higher-level interfaces which track DNSKEY flags information (and possibly key rollover information). * [sign/ring] Store the RNG in an 'Arc' * [validate] Enhance 'Signature' API * [validate] Add high-level 'Key' type * [sign/openssl] Pad ECDSA keys when exporting Tests would spuriously fail when generated keys were only 31 bytes in size. * [validate] Implement 'Key::key_tag()' This is more efficient than allocating a DNSKEY record and computing the key tag there. * [validate] Correct bit offsets for flags * [validate] Implement support for digests The test keys have been rotated and replaced with KSKs since they have associated DS records I can verify digests against. I also expanded Ring's testing to include ECDSA keys. The validate module tests SHA-1 keys as well, which aren't supported by 'sign'. * [validate] Enhance BIND format conversion for 'Key' Public keys in the BIND format can now have multiple lines (even with comments). Keys can also be directly written into the BIND format and round-trips to and from the BIND format are now tested. * [sign] Introduce 'SigningKey' * [sign] Handle errors more responsibly The 'openssl' and 'ring' modules should now follow the contributing guidelines regarding module layout and formatting. * [sign] correct doc link * [sign/openssl] Replace panics with results * remove 'sign/key' * [sign] Introduce 'common' for abstracting backends This is useful for abstracting over OpenSSL and Ring, so that Ring can be used whenever possible while OpenSSL is used as a fallback. This is useful for clients that just wish to support everything. * [sign/generic] add top-level doc comment * [validate] debug bind format errors * [validate] more debug statements * [validate] format DNSKEYs using 'ZonefileFmt' The 'Dnskey' impl of 'fmt::Display' was no longer accurate to the zone file format because 'SecAlg' now prints '()'. * Reorganize crate features in 'Cargo.toml' * [sign] Add key generation support for Ring It's a bit hacky because it relies on specific byte indices within the generated PKCS8 documents (internally, Ring basically just concatenates bytes to form the documents, and we use the same indices). However, any change to the document format should be caught by the tests here. * [sign] Make OpenSSL support optional Now that Ring and OpenSSL support all mandatory algorithms, OpenSSL is no longer required in order to provide signing functionality. * [sign] Rename 'generic::SecretKey' to 'KeyBytes' * [sign] Rename 'SecretKey' to 'KeyPair' in all impls * [sign] Rename 'KeyBytes' to 'SecretKeyBytes' For consistency with the upcoming 'PublicKeyBytes'. * [validate] Rename 'RawPublicKey' to 'PublicKeyBytes' * [sign/ring] Remove redundant imports * [sign,validate] Add 'display_as_bind()' to key bytes types * [sign,validate] remove unused imports * [sign] Document everything * [lib] Rewrite feature flag documentation * [workflows/ci] Use 'apt-get' instead of 'apt' * [sign] Clarify documentation as per @ximon18 * [sign] Use 'secrecy' to protect private keys * [sign] Improve documentation and examples --------- Co-authored-by: arya dradjica --- .github/workflows/ci.yml | 17 + Cargo.lock | 76 ++ Cargo.toml | 20 +- src/lib.rs | 102 +- src/sign/bytes.rs | 509 ++++++++++ src/sign/common.rs | 274 +++++ src/sign/key.rs | 53 - src/sign/mod.rs | 414 +++++++- src/sign/openssl.rs | 598 ++++++++++- src/sign/ring.rs | 507 ++++++++-- src/validate.rs | 944 +++++++++++++++++- test-data/dnssec-keys/Ktest.+005+00439.ds | 1 + test-data/dnssec-keys/Ktest.+005+00439.key | 1 + .../dnssec-keys/Ktest.+005+00439.private | 10 + test-data/dnssec-keys/Ktest.+007+22204.ds | 1 + test-data/dnssec-keys/Ktest.+007+22204.key | 1 + .../dnssec-keys/Ktest.+007+22204.private | 10 + test-data/dnssec-keys/Ktest.+008+60616.ds | 1 + test-data/dnssec-keys/Ktest.+008+60616.key | 1 + .../dnssec-keys/Ktest.+008+60616.private | 10 + test-data/dnssec-keys/Ktest.+013+42253.ds | 1 + test-data/dnssec-keys/Ktest.+013+42253.key | 1 + .../dnssec-keys/Ktest.+013+42253.private | 3 + test-data/dnssec-keys/Ktest.+014+33566.ds | 1 + test-data/dnssec-keys/Ktest.+014+33566.key | 1 + .../dnssec-keys/Ktest.+014+33566.private | 3 + test-data/dnssec-keys/Ktest.+015+56037.ds | 1 + test-data/dnssec-keys/Ktest.+015+56037.key | 1 + .../dnssec-keys/Ktest.+015+56037.private | 3 + test-data/dnssec-keys/Ktest.+016+07379.ds | 1 + test-data/dnssec-keys/Ktest.+016+07379.key | 1 + .../dnssec-keys/Ktest.+016+07379.private | 3 + 32 files changed, 3313 insertions(+), 257 deletions(-) create mode 100644 src/sign/bytes.rs create mode 100644 src/sign/common.rs delete mode 100644 src/sign/key.rs create mode 100644 test-data/dnssec-keys/Ktest.+005+00439.ds create mode 100644 test-data/dnssec-keys/Ktest.+005+00439.key create mode 100644 test-data/dnssec-keys/Ktest.+005+00439.private create mode 100644 test-data/dnssec-keys/Ktest.+007+22204.ds create mode 100644 test-data/dnssec-keys/Ktest.+007+22204.key create mode 100644 test-data/dnssec-keys/Ktest.+007+22204.private create mode 100644 test-data/dnssec-keys/Ktest.+008+60616.ds create mode 100644 test-data/dnssec-keys/Ktest.+008+60616.key create mode 100644 test-data/dnssec-keys/Ktest.+008+60616.private create mode 100644 test-data/dnssec-keys/Ktest.+013+42253.ds create mode 100644 test-data/dnssec-keys/Ktest.+013+42253.key create mode 100644 test-data/dnssec-keys/Ktest.+013+42253.private create mode 100644 test-data/dnssec-keys/Ktest.+014+33566.ds create mode 100644 test-data/dnssec-keys/Ktest.+014+33566.key create mode 100644 test-data/dnssec-keys/Ktest.+014+33566.private create mode 100644 test-data/dnssec-keys/Ktest.+015+56037.ds create mode 100644 test-data/dnssec-keys/Ktest.+015+56037.key create mode 100644 test-data/dnssec-keys/Ktest.+015+56037.private create mode 100644 test-data/dnssec-keys/Ktest.+016+07379.ds create mode 100644 test-data/dnssec-keys/Ktest.+016+07379.key create mode 100644 test-data/dnssec-keys/Ktest.+016+07379.private diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de6bf224b..02a0af673 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 58dfde030..ca7fb4b69 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 b1bda94ef..4c60ad9d7 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 6d6cfd344..0d0a4a2ba 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 000000000..6393a0aca --- /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 000000000..fe0fd1113 --- /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 da9385780..000000000 --- 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 d87acca0c..7adbb607a 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. + +#![cfg(feature = "unstable-sign")] +#![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] -pub mod key; -//pub mod openssl; -pub mod records; +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 c49512b73..a7250081a 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; + +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, +} + +//--- Conversion to and from bytes + +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, + }) + } -pub struct Key { - dnskey: Dnskey>, - key: PKey, - digest: MessageDigest, + /// 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!(), + } + } } -impl SigningKey for Key { - type Octets = Vec; - type Signature = Vec; - type Error = ErrorStack; +//--- 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) + } - fn dnskey(&self) -> Result, Self::Error> { - Ok(self.dnskey.clone()) + 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!(), + } } +} - 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, - )) +//--- 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(&self, data: &[u8]) -> Result { - let mut signer = OpenSslSigner::new( - self.digest, &self.key - )?; - signer.update(data)?; - signer.sign_to_vec() + 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 bf4614f2b..d1e29c395 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}, +}; + +use super::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; + +//----------- KeyPair -------------------------------------------------------- -#[allow(dead_code, clippy::large_enum_variant)] -enum RingKey { - Ecdsa(EcdsaKeyPair), +/// 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), + } } } -impl<'a> SigningKey for Key<'a> { - type Octets = Vec; - type Signature = Signature; - type Error = Unspecified; +//--- SignRaw - fn dnskey(&self) -> Result, Self::Error> { - Ok(self.dnskey.clone()) +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 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")) + 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(&self, msg: &[u8]) -> Result { - match self.key { - RingKey::Ecdsa(ref key) => { - key.sign(self.rng, msg).map(Signature::sig) + 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) } - 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)) + + 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) } } } } -pub struct Signature(SignatureInner); +//----------- 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 =================================================== -enum SignatureInner { - Sig(RingSignature), - Vec(Vec), +/// 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, } -impl Signature { - fn sig(sig: RingSignature) -> Signature { - Signature(SignatureInner::Sig(sig)) +//--- 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", + }) } +} + +//--- Error + +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, - fn vec(vec: Vec) -> Signature { - Signature(SignatureInner::Vec(vec)) + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +impl From for GenerateError { + fn from(_: ring::error::Unspecified) -> Self { + Self::Implementation } } -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(), - } +//--- 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", + }) } } -#[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), +//--- 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); + } + } + + #[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); + } + } + + #[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 41b7456e5..0f307fb42 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 000000000..543137100 --- /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 000000000..35999a0ae --- /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 000000000..1d8d11ce6 --- /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 000000000..913575095 --- /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 000000000..26bf24bfc --- /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 000000000..ecb576d4c --- /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 000000000..65444f942 --- /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 000000000..fa6c03d8a --- /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 000000000..8df7cdc20 --- /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 000000000..8d52a1301 --- /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 000000000..c9d6127ea --- /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 000000000..7b26e96a1 --- /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 000000000..7e3165c6c --- /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 000000000..dd967bccb --- /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 000000000..276b9d315 --- /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 000000000..fb802353f --- /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 000000000..38dc516a9 --- /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 000000000..52c5034aa --- /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 000000000..a1ca41c42 --- /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 000000000..a7eade4f9 --- /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 000000000..9d837bcc4 --- /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