diff --git a/Cargo.lock b/Cargo.lock index a5d780698..14bcd6817 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -80,6 +80,22 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bcder" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7c42c9913f68cf9390a225e81ad56a5c515347287eb98baa710090ca1de86d" +dependencies = [ + "bytes", + "smallvec", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -193,7 +209,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -202,12 +218,24 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "domain" version = "0.11.1" dependencies = [ "arbitrary", "arc-swap", + "bcder", "bumpalo", "bytes", "chrono", @@ -217,6 +245,7 @@ dependencies = [ "hashbrown", "heapless", "itertools", + "kmip-protocol", "lazy_static", "libc", "log", @@ -230,7 +259,7 @@ dependencies = [ "rand", "ring", "rstest", - "rustls-pemfile", + "rustls-pemfile 2.2.0", "rustversion", "secrecy", "serde", @@ -248,6 +277,8 @@ dependencies = [ "tokio-tfo", "tracing", "tracing-subscriber", + "url", + "uuid", "webpki-roots 0.26.11", ] @@ -257,7 +288,7 @@ version = "0.11.1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -266,6 +297,28 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "enum-display-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ef37b2a9b242295d61a154ee91ae884afff6b8b933b486b12481cc58310ca" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "enum-flags" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3682d2328e61f5529088a02cd20bb0a9aeaeeeb2f26597436dd7d75d1340f8f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -314,6 +367,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.31" @@ -370,7 +432,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -466,6 +528,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -490,6 +558,108 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.12.0" @@ -525,6 +695,44 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kmip-protocol" +version = "0.5.0" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#ad08d63ffd9bbb96ec29d1272d08244e86ed74e6" +dependencies = [ + "cfg-if", + "enum-display-derive", + "enum-flags", + "hex", + "kmip-ttlv", + "log", + "maybe-async", + "r2d2", + "rustc_version", + "rustls", + "rustls-pemfile 0.2.1", + "serde", + "serde_bytes", + "serde_derive", + "tracing", + "trait-set", + "webpki-roots 1.0.3", +] + +[[package]] +name = "kmip-ttlv" +version = "0.4.0" +source = "git+https://github.com/NLnetLabs/kmip-ttlv?branch=next#4ca144e19e69375a6ccd63cf40b0e61f89462f97" +dependencies = [ + "cfg-if", + "hex", + "maybe-async", + "rustc_version", + "serde", + "tracing", + "trait-set", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -537,6 +745,12 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "lock_api" version = "0.4.14" @@ -561,6 +775,17 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "maybe-async" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "memchr" version = "2.7.6" @@ -670,7 +895,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -714,6 +939,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project" version = "1.1.10" @@ -731,7 +962,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -758,6 +989,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -816,6 +1056,17 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.8.5" @@ -929,7 +1180,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn", + "syn 2.0.108", "unicode-ident", ] @@ -957,6 +1208,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -998,6 +1258,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1029,6 +1298,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -1046,7 +1325,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -1149,6 +1428,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.108" @@ -1160,6 +1450,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -1206,6 +1507,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.48.0" @@ -1229,7 +1540,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -1333,7 +1644,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -1375,6 +1686,17 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trait-set" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875c4c873cc824e362fa9a9419ffa59807244824275a44ad06fec9684fff08f2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "unicode-ident" version = "1.0.20" @@ -1393,6 +1715,24 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.18.1" @@ -1454,7 +1794,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.108", "wasm-bindgen-shared", ] @@ -1476,7 +1816,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1529,7 +1869,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -1540,7 +1880,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", ] [[package]] @@ -1804,12 +2144,41 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -1827,7 +2196,28 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.108", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "synstructure", ] [[package]] @@ -1835,3 +2225,36 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] diff --git a/Cargo.toml b/Cargo.toml index 76e39ec2e..b2e45f907 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ chrono = { version = "0.4.35", optional = true, default-features = false futures-util = { version = "0.3", optional = true } hashbrown = { version = "0.16.0", optional = true, default-features = false, features = ["allocator-api2", "inline-more"] } # 0.14.2 introduces explicit hashing heapless = { version = "0.8", optional = true } +kmip = { git = "https://github.com/NLnetLabs/kmip-protocol", branch = "next", package = "kmip-protocol", version = "0.5.0", optional = true, features = ["tls-with-rustls"] } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT log = { version = "0.4.22", optional = true } parking_lot = { version = "0.12", optional = true } @@ -41,6 +42,7 @@ moka = { version = "0.12.3", optional = true, features = ["future"] } openssl = { version = "0.10.72", optional = true } # 0.10.70 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.2", optional = true } +bcder = { version = "0.7", optional = true } rustversion = { version = "1", optional = true } secrecy = { version = "0.10", optional = true } serde = { version = "1.0.130", optional = true, features = ["derive"] } @@ -48,9 +50,11 @@ siphasher = { version = "1", optional = true } smallvec = { version = "1.3", optional = true } tokio = { version = "1.33", optional = true, features = ["io-util", "macros", "net", "time", "sync", "rt-multi-thread" ] } tokio-rustls = { version = "0.26", optional = true, default-features = false } -tokio-stream = { version = "0.1.1", optional = true } +tokio-stream = { version = "0.1.17", optional = true } tracing = { version = "0.1.40", optional = true, features = ["log"] } tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-filter"] } +url = { version = "2.5.4", optional = true } +uuid = { version = "1.17.0", optional = true, features = ["v4"] } [features] default = ["std", "rand"] @@ -66,8 +70,9 @@ std = ["alloc", "dep:hashbrown", "bumpalo?/std", "bytes?/std", "octseq/s tracing = ["dep:log", "dep:tracing"] # Cryptographic backends -ring = ["dep:ring"] -openssl = ["dep:openssl"] +ring = ["dep:ring"] +openssl = ["dep:openssl"] +kmip = ["dep:kmip", "dep:bcder", "dep:url", "dep:uuid", "dep:openssl"] # Crate features net = ["bytes", "futures-util", "rand", "std", "tokio"] diff --git a/Changelog.md b/Changelog.md index 57c5d2328..1711eaf2a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -16,6 +16,8 @@ New record types and added presentation format support for the `SVCB`/`HTTPS` record types. ([#569]) * Add support for the `CAA` record type. ([#434] by [@weilence]) +* Added `FreezeBuilder` to the message compressors. ([#601] by + [@rossmacarthur]) Improvements @@ -76,9 +78,11 @@ Other changes [#570]: https://github.com/NLnetLabs/domain/pull/570 [#593]: https://github.com/NLnetLabs/domain/pull/593 [#594]: https://github.com/NLnetLabs/domain/pull/594 +[#601]: https://github.com/NLnetLabs/domain/pull/601 [@rossmacarthur]: https://github.com/rossmacarthur [@weilence]: https://github.com/weilence [@WhyNotHugo]: https://github.com/WhyNotHugo +[@rossmacarthur]: https://github.com/rossmacarthur ## 0.11.1 diff --git a/src/base/message_builder.rs b/src/base/message_builder.rs index 21ff700ce..32566c11a 100644 --- a/src/base/message_builder.rs +++ b/src/base/message_builder.rs @@ -2373,6 +2373,15 @@ impl Truncate for TreeCompressor { } } +#[cfg(feature = "std")] +impl FreezeBuilder for TreeCompressor { + type Octets = Target::Octets; + + fn freeze(self) -> Self::Octets { + self.target.freeze() + } +} + //------------ HashCompressor ------------------------------------------------ /// A domain name compressor that uses a hash table. @@ -2639,6 +2648,15 @@ impl Truncate for HashCompressor { } } +#[cfg(feature = "std")] +impl FreezeBuilder for HashCompressor { + type Octets = Target::Octets; + + fn freeze(self) -> Self::Octets { + self.target.freeze() + } +} + //============ Errors ======================================================== /// An error occurred when attempting to add data to a message. @@ -2880,6 +2898,22 @@ mod test { assert_eq!(&expect[..], msg.as_ref()); } + // just check into_message compiles for all compressors + #[test] + fn compressor_into_message() { + let target = StaticCompressor::new(Vec::new()); + let _msg = + MessageBuilder::from_target(target).unwrap().into_message(); + + let target = TreeCompressor::new(Vec::new()); + let _msg = + MessageBuilder::from_target(target).unwrap().into_message(); + + let target = HashCompressor::new(Vec::new()); + let _msg = + MessageBuilder::from_target(target).unwrap().into_message(); + } + #[test] fn compress_positive_response() { // An example positive response to `A example.com.` that is compressed diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs new file mode 100644 index 000000000..68b2eeee3 --- /dev/null +++ b/src/crypto/kmip.rs @@ -0,0 +1,1621 @@ +//! DNSSEC signing using OASIS KMIP (Key Management Interoperability Protocol). +#![cfg(all(feature = "kmip", any(feature = "ring", feature = "openssl")))] +#![cfg_attr(docsrs, doc(cfg(feature = "kmip")))] + +use core::{fmt, str::FromStr}; + +use std::{ + string::{String, ToString}, + vec::Vec, +}; + +use bcder::{decode::SliceSource, BitString, ConstOid, Oid}; +use kmip::{ + client::pool::SyncConnPool, + types::{ + common::{KeyFormatType, KeyMaterial, TransparentRSAPublicKey}, + response::ManagedObject, + }, +}; +use tracing::{debug, error}; +use url::Url; + +use crate::{ + base::iana::SecurityAlgorithm, + crypto::{common::rsa_encode, sign::SignError}, + rdata::Dnskey, + utils::base16, +}; + +pub use kmip::client::{ClientCertificate, ConnectionSettings}; + +//------------ Constants ----------------------------------------------------- + +/// [RFC 4055](https://tools.ietf.org/html/rfc4055) `rsaEncryption` +/// +/// Identifies an RSA public key with no limitation to either RSASSA-PSS or +/// RSAES-OEAP. +pub const RSA_ENCRYPTION_OID: ConstOid = + Oid(&[42, 134, 72, 134, 247, 13, 1, 1, 1]); + +/// [RFC 5480](https://tools.ietf.org/html/rfc5480) `ecPublicKey`. +/// +/// Identifies public keys for elliptic curve cryptography. +pub const EC_PUBLIC_KEY_OID: ConstOid = Oid(&[42, 134, 72, 206, 61, 2, 1]); + +/// [RFC 5480](https://tools.ietf.org/html/rfc5480) `secp256r1`. +/// +/// Identifies the P-256 curve for elliptic curve cryptography. +pub const SECP256R1_OID: ConstOid = Oid(&[42, 134, 72, 206, 61, 3, 1, 7]); + +//------------ KeyUrl -------------------------------------------------------- + +/// A URL that represents a key stored in a KMIP compatible HSM. +/// +/// The URL structure is: +/// +/// kmip:///keys/?algorithm=&flags= +/// +/// The algorithm and flags must be stored in the URL because they are DNSSEC +/// specific and not properties of the key itself and thus not known to or +/// stored by the HSM. +/// +/// While algorithm may seem to be something known to and stored by the HSM, +/// DNSSEC complicates that by aliasing multiple algorithm numbers to the +/// same cryptographic algorithm, and we need to know when using the key which +/// _DNSSEC_ algorithm number to use. +/// +/// The server_id could be the actual address of the target, but does not have +/// to be. There are multiple for this: +/// +/// - In a highly available clustered deployment across multiple subnets +/// it could be that the clustered HSM is available to the clustered +/// application via different names/IP addresses in different subnets of +/// the deployment. Using an abstract server_id which is mapped via local +/// configuration in the subnet to the correct hostname/FQDN/IP address +/// for that subnet allows the correct target address to be determined at +/// the point of access. +/// - Using the actual hostname/FQDN/IP address may make it confusing for +/// an operator trying to understand where the key is actually stored. +/// This can happen for example if the product name for the HSM is say +/// Fortanix DSM, while the domain name used to access the HSM might be +/// eu.smartkey.io, which having no mention of the name Fortanix in the +/// FQDN is not immediately obvious that it has any relationship with +/// Fortanix. +/// - If the same HSM is used for different use cases via use of HSM +/// partitions, referring to the HSM by its address may not make it clear +/// which partition is being used, so using a more meaningful name like +/// 'testing' or such could make it clearer where the key is actually +/// being stored. +/// - Storing the username and password in the key URL will cause many +/// copies of those credentials to be stored, one per key, which is harder +/// to secure than if they are only in a single location and looked up on +/// actual access. +/// - Storing the username and password in the key URL would cause the URL +/// to become unusable if the credentials were rotated even though the +/// location at which the key is stored has not changed. +/// - Even if the FQDN, port number, username and password are all correct, +/// there may need to be more settings specified in order to connect to +/// the HSM some of which would not fit easily into a URL such as TLS +/// client certficate details and whether or not to require the server +/// TLS certificate to be valid (which can be inconvenient in test setups +/// using self-signed certificates). +/// +/// Thus an abstract server_id is stored in the key URL and it is the +/// responsibility of the user of the key URL to map the server id to the full +/// set of settings required to successfully connect to the HSM to make use of +/// the key. +pub struct KeyUrl { + /// The original URL from which this KeyUrl was parsed. + url: Url, + + /// The KMIP server ID. Produced by the application. + server_id: String, + + /// The KMIP key ID. Produced by the KMIP server. + key_id: String, + + /// The DNSSEC algorithm this key is to be used for. + algorithm: SecurityAlgorithm, + + /// The DNSSEC flags that apply to this key. + flags: u16, +} + +//--- Accessors + +impl KeyUrl { + /// The KMIP server ID. + pub fn server_id(&self) -> &str { + &self.server_id + } + + /// The KMIP key ID. + pub fn key_id(&self) -> &str { + &self.key_id + } + + /// The DNSSEC algorithm identifier for the key. + pub fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + /// The DNSSEC flags for the key. + pub fn flags(&self) -> u16 { + self.flags + } +} + +//--- impl Into + +// Disablow the Clippy lint as it is safe to go from a KeyURL to a URL but +// not vice-versa, so we implement Into but not From. +#[allow(clippy::from_over_into)] +impl Into for KeyUrl { + fn into(self) -> Url { + self.url + } +} + +//--- impl Deref + +impl std::ops::Deref for KeyUrl { + type Target = Url; + + fn deref(&self) -> &Self::Target { + &self.url + } +} + +//--- Conversions + +impl TryFrom for KeyUrl { + type Error = String; + + fn try_from(url: Url) -> Result { + let server_id = url + .host_str() + .ok_or(format!("Key URL lacks hostname component: {url}"))? + .to_string(); + + let url_path = url.path().to_string(); + let key_id = url_path + .strip_prefix("/keys/") + .ok_or(format!("Key URL lacks /keys/ path component: {url}"))?; + + let key_id = key_id.to_string(); + let mut flags = None; + let mut algorithm = None; + for (k, v) in url.query_pairs() { + match &*k { + "flags" => { + flags = Some(v.parse::().map_err(|err| { + format!("Key URL flags value is invalid: {err}") + })?) + } + "algorithm" => { + algorithm = Some( + SecurityAlgorithm::from_str(&v).map_err(|err| { + format!( + "Key URL algorithm value is invalid: {err}" + ) + })?, + ) + } + unknown => Err(format!( + "Key URL contains unknown query parameter: {unknown}" + ))?, + } + } + let algorithm = algorithm.ok_or(format!( + "Key URL lacks algorithm query parameter: {url}" + ))?; + let flags = flags + .ok_or(format!("Key URL lacks flags query parameter: {url}"))?; + + Ok(Self { + url, + server_id, + key_id, + algorithm, + flags, + }) + } +} + +//--- impl Display + +impl std::fmt::Display for KeyUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.url.fmt(f) + } +} + +//------------ PublicKey ----------------------------------------------------- + +/// A public key for verifying a signature. +pub struct PublicKey { + /// The DNSSEC algorithm for use with this public key. + algorithm: SecurityAlgorithm, + + /// The public key octets. + public_key: Vec, +} + +impl PublicKey { + /// Create a public key from a key stored on a KMIP server. + /// + /// The public key details will be retrieved from the KMIP server. + /// + /// The DNSSEC algorithm is needed in order for [`Self::dnskey()`] to + /// generate a [`Dnskey`] and must match the cryptographic algorithm of + /// the key stored on the KMIP server. + /// + /// Note: This function will block while awaiting the response from the + /// KMIP server. + /// + /// If the KMIP operation fails an error or the response cannot be parsed + /// an error will be returned. + /// + /// If the cryptographic algorithm of the retrieved key does not match + /// the given DNSSEC algorithm an error will be returned. + pub fn for_key_id_and_dnssec_algorithm( + public_key_id: &str, + algorithm: SecurityAlgorithm, + conn_pool: SyncConnPool, + ) -> Result { + let public_key = + Self::fetch_public_key(public_key_id, algorithm, &conn_pool)?; + + Ok(Self { + algorithm, + public_key, + }) + } + + /// Create a public key from a key stored on a KMIP server. + /// + /// This is a thin wrapper around + /// [`Self::for_key_id_and_dnssec_algorithm`]. + pub fn for_key_url( + public_key_url: KeyUrl, + conn_pool: SyncConnPool, + ) -> Result { + Self::for_key_id_and_dnssec_algorithm( + public_key_url.key_id(), + public_key_url.algorithm(), + conn_pool, + ) + } + + /// The DNSSEC algorithm of the key. + pub fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + /// Generate a DNSKEY RR or this public key. + pub fn dnskey(&self, flags: u16) -> Dnskey> { + // SAFETY: The key came from a KMIP server and was validated to have + // the expected length when the KMIP server response was parsed by + // fetch_public_key(). + Dnskey::new(flags, 3, self.algorithm, self.public_key.clone()) + .unwrap() + } +} + +impl PublicKey { + /// Query the KMIP server for the bytes of the specified public key. + /// + /// Verifies that the cryptographic algorithm of the key is compatible + /// with the specified DNSSEC algorithm. + fn fetch_public_key( + public_key_id: &str, + expected_algorithm: SecurityAlgorithm, + conn_pool: &SyncConnPool, + ) -> Result, PublicKeyError> { + // https://datatracker.ietf.org/doc/html/rfc5702#section-2 + // Use of SHA-2 Algorithms with RSA in DNSKEY and RRSIG Resource + // Records for DNSSEC + // + // 2. DNSKEY Resource Records + // "The format of the DNSKEY RR can be found in [RFC4034]. [RFC3110] + // describes the use of RSA/SHA-1 for DNSSEC signatures." + // | + // | + // v + // https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.4 + // Resource Records for the DNS Security Extensions + // 2. The DNSKEY Resource Record + // 2.1.4. The Public Key Field + // "The Public Key Field holds the public key material. The + // format depends on the algorithm of the key being stored and is + // described in separate documents." + // | + // | + // v + // https://datatracker.ietf.org/doc/html/rfc3110#section-2 + // RSA/SHA-1 SIGs and RSA KEYs in the Domain Name System (DNS) + // 2. RSA Public KEY Resource Records + // "... The structure of the algorithm specific portion of the RDATA + // part of such RRs is as shown below. + // + // Field Size + // ----- ---- + // exponent length 1 or 3 octets (see text) + // exponent as specified by length field + // modulus remaining space + // + // For interoperability, the exponent and modulus are each limited to + // 4096 bits in length. The public key exponent is a variable length + // unsigned integer. Its length in octets is represented as one octet + // if it is in the range of 1 to 255 and by a zero octet followed by + // a two octet unsigned length if it is longer than 255 bytes. The + // public key modulus field is a multiprecision unsigned integer. The + // length of the modulus can be determined from the RDLENGTH and the + // preceding RDATA fields including the exponent. Leading zero octets + // are prohibited in the exponent and modulus. + + let client = conn_pool.get().inspect_err(|err| error!("{err}")).map_err(|err| { + kmip::client::Error::ServerError(format!( + "Error while attempting to acquire KMIP connection from pool: {err}" + )) + })?; + + // Note: OpenDNSSEC queries the public key ID, _unless_ it was + // configured not the public key in the HSM (by setting CKA_TOKEN + // false) in which case there is no public key and so it uses the + // private key object handle instead. + let res = client + .get_key(public_key_id) + .inspect_err(|err| error!("{err}"))?; + let ManagedObject::PublicKey(public_key) = res.cryptographic_object + else { + return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object was expected to be a PublicKey but was instead: {}", res.cryptographic_object)))?; + }; + + // https://docs.oasis-open.org/kmip/ug/v1.2/cn01/kmip-ug-v1.2-cn01.html#_Toc407027125 + // "“Raw” key format is intended to be applied to symmetric keys + // and not asymmetric keys" + // + // As we deal in asymmetric keys (RSA, ECDSA), not symmetric keys, + // we should not encounter public_key.key_block.key_format_type + // == KeyFormatType::Raw. However, Fortanix DSM returns + // KeyFormatType::Raw when fetching key data for an ECDSA public key. + + let octets = match public_key.key_block.key_value.key_material { + KeyMaterial::Bytes(bytes) => { + debug!("Cryptographic Algorithm: {:?}", public_key.key_block.cryptographic_algorithm); + debug!("Cryptographic Length: {:?}", public_key.key_block.cryptographic_length); + debug!("Key Format Type: {:?}", public_key.key_block.key_format_type); + debug!("Key Compression Type: {:?}", public_key.key_block.key_compression_type); + debug!("Key bytes as hex: {}", base16::encode_display(&bytes)); + + match (expected_algorithm, public_key.key_block.key_format_type) { + (SecurityAlgorithm::RSASHA1, KeyFormatType::PKCS1) | + (SecurityAlgorithm::RSASHA1_NSEC3_SHA1, KeyFormatType::PKCS1) | + (SecurityAlgorithm::RSASHA256, KeyFormatType::PKCS1) | + (SecurityAlgorithm::RSASHA512, KeyFormatType::PKCS1) => { + // PyKMIP outputs PKCS#1 ASN.1 DER encoded RSA public + // key data like so: + // RSAPublicKey::=SEQUENCE{ + // modulus INTEGER, -- n + // publicExponent INTEGER -- e } + let source = SliceSource::new(&bytes); + let mut modulus = None; + let mut public_exponent = None; + bcder::Mode::Der + .decode(source, |cons| { + cons.take_sequence(|cons| { + modulus = Some(bcder::Unsigned::take_from(cons)?); + public_exponent = Some(bcder::Unsigned::take_from(cons)?); + Ok(()) + }) + }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse DER encoded PKCS#1 RSAPublicKey: {err}")))?; + + let Some(modulus) = modulus else { + return Err(kmip::client::Error::DeserializeError("Unable to parse DER encoded PKCS#1 RSAPublicKey: missing modulus".into()))?; + }; + + let Some(public_exponent) = public_exponent else { + return Err(kmip::client::Error::DeserializeError("Unable to parse DER encoded PKCS#1 RSAPublicKey: missing public exponent".into()))?; + }; + + let n = modulus.as_slice(); + let e = public_exponent.as_slice(); + crate::crypto::common::rsa_encode(e, n) + }, + + (SecurityAlgorithm::RSASHA1, KeyFormatType::Raw) | + (SecurityAlgorithm::RSASHA1_NSEC3_SHA1, KeyFormatType::Raw) | + (SecurityAlgorithm::RSASHA256, KeyFormatType::Raw) | + (SecurityAlgorithm::RSASHA512, KeyFormatType::Raw) => { + // For an RSA key Fortanix DSM supplies: (from https://asn1js.eu/) + // SubjectPublicKeyInfo SEQUENCE (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE (2 elem) + // algorithm OBJECT IDENTIFIER 1.2.840.113549.1.1.1 rsaEncryption (PKCS #1) + // parameter ANY NULL + // subjectPublicKey BIT STRING (2160 bit) 001100001000001000000001000010100000001010000010000000010000000100000… + // SEQUENCE (2 elem) + // INTEGER (2048 bit) 229677698057230630160769379936346719377896297586216888467726484346678… + // INTEGER 65537 + let source = SliceSource::new(&bytes); + let mut modulus = None; + let mut public_exponent = None; + bcder::Mode::Der + .decode(source, |cons| { + cons.take_sequence(|cons| { + cons.take_sequence(|cons| { + let algorithm = Oid::take_from(cons)?; + if algorithm != RSA_ENCRYPTION_OID { + return Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm rsaEncryption is supported")); + } + // Ignore the parameters. + Ok(()) + })?; + cons.take_sequence(|cons| { + modulus = Some(bcder::Unsigned::take_from(cons)?); + public_exponent = Some(bcder::Unsigned::take_from(cons)?); + Ok(()) + }) + }) + }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: {err}")))?; + + let Some(modulus) = modulus else { + return Err(kmip::client::Error::DeserializeError("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: missing modulus".into()))?; + }; + + let Some(public_exponent) = public_exponent else { + return Err(kmip::client::Error::DeserializeError("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: missing public exponent".into()))?; + }; + + let n = modulus.as_slice(); + let e = public_exponent.as_slice(); + crate::crypto::common::rsa_encode(e, n) + } + + (SecurityAlgorithm::ECDSAP256SHA256, KeyFormatType::Raw) => { + // For an ECDSA key Fortanix DSM supplies: (from https://asn1js.eu/) + // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) + // algorithm OBJECT_IDENTIFIER @4+7: 1.2.840.10045.2.1|ecPublicKey|ANSI X9.62 public key type + // parameters ANY OBJECT_IDENTIFIER @13+8: 1.2.840.10045.3.1.7|prime256v1|ANSI X9.62 named elliptic curve + // subjectPublicKey BIT_STRING @23+66: (520 bit) + // + // From: https://www.rfc-editor.org/rfc/rfc5480.html#section-2.1.1 + // The parameter for id-ecPublicKey is as follows and MUST always be + // present: + // + // ECParameters ::= CHOICE { + // namedCurve OBJECT IDENTIFIER + // -- implicitCurve NULL + // -- specifiedCurve SpecifiedECDomain + // } + // -- implicitCurve and specifiedCurve MUST NOT be used in PKIX. + // -- Details for SpecifiedECDomain can be found in [X9.62]. + // -- Any future additions to this CHOICE should be coordinated + // -- with ANSI X9. + let source = SliceSource::new(&bytes); + let mut bits = None; + bcder::Mode::Der + .decode(source, |cons| { + cons.take_sequence(|cons| { + cons.take_sequence(|cons| { + let algorithm = Oid::take_from(cons)?; + if algorithm != EC_PUBLIC_KEY_OID { + Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm id-ecPublicKey is supported")) + } else { + let named_curve = Oid::take_from(cons)?; + if named_curve != SECP256R1_OID { + return Err(cons.content_err("Only SubjectPublicKeyInfo with namedCurve secp256r1 is supported")); + } + Ok(()) + } + })?; + bits = Some(BitString::take_from(cons)?); + Ok(()) + }) + }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo: {err}")))?; + + let Some(bits) = bits else { + return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into()))?; + }; + + // https://www.rfc-editor.org/rfc/rfc5480#section-2.2 + // "The subjectPublicKey from SubjectPublicKeyInfo + // is the ECC public key. ECC public keys have the + // following syntax: + // + // ECPoint ::= OCTET STRING + // ... + // The first octet of the OCTET STRING indicates + // whether the key is compressed or uncompressed. + // The uncompressed form is indicated by 0x04 and + // the compressed form is indicated by either 0x02 + // or 0x03 (see 2.3.3 in [SEC1]). The public key + // MUST be rejected if any other value is included + // in the first octet." + let Some(octets) = bits.octet_slice() else { + return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into()))?; + }; + + // Expect octet string to be [, + // <32-byte X value>, <32-byte Y value>]. + if octets.len() != 65 { + return Err(kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: expected [, <32-byte X value>, <32-byte Y value>]: {} ({} bytes)", base16::encode_display(octets), octets.len())))?; + } + + // Note: OpenDNSSEC doesn't support the compressed + // form either. + let compression_flag = octets[0]; + if compression_flag != 0x04 { + return Err(kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: unknown compression flag {compression_flag:?}")))?; + } + + // Expect octet string to be X | Y (| denotes + // concatenation) where X and Y are each 32 bytes + // (because P-256 uses 256 bit values and 256 bits are + // 32 bytes). Skip the compression flag. + octets[1..].to_vec() + } + + (expected, key_format_type) => { + let alg = public_key.key_block.cryptographic_algorithm.map(|a| a.to_string()).unwrap_or("unknown algorithm".to_string()); + let len = public_key.key_block.cryptographic_length.map(|l| l.to_string()).unwrap_or("unknown length".to_string()); + let actual = format!("{alg} ({len}) as {key_format_type}"); + return Err(PublicKeyError::AlgorithmMismatch { expected, actual }); + } + } + } + + KeyMaterial::TransparentRSAPublicKey( + // Nameshed-HSM-Relay + TransparentRSAPublicKey { + modulus, + public_exponent, + }, + ) => rsa_encode(&public_exponent, &modulus), + + mat => return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object has unsupported key material type: {mat}")))?, + }; + + Ok(octets) + } +} + +//============ sign ========================================================== + +#[cfg(feature = "unstable-crypto-sign")] +/// Submodule for private keys and signing. +pub mod sign { + use std::boxed::Box; + use std::string::{String, ToString}; + use std::time::SystemTime; + use std::vec::Vec; + + use kmip::client::pool::SyncConnPool; + use kmip::types::common::{ + CryptographicAlgorithm, CryptographicParameters, + CryptographicUsageMask, Data, DigitalSignatureAlgorithm, + HashingAlgorithm, PaddingMethod, UniqueBatchItemID, UniqueIdentifier, + }; + use kmip::types::request::{ + self, BatchItem, CommonTemplateAttribute, + PrivateKeyTemplateAttribute, PublicKeyTemplateAttribute, + RequestPayload, + }; + use kmip::types::response::{ + CreateKeyPairResponsePayload, ResponsePayload, + }; + use log::trace; + use openssl::ecdsa::EcdsaSig; + use tracing::{debug, error}; + use url::Url; + use uuid::Uuid; + + use crate::base::iana::SecurityAlgorithm; + use crate::crypto::common::DigestType; + use crate::crypto::kmip::{ + DestroyError, GenerateError, KeyUrl, KeyUrlParseError, PublicKey, + }; + use crate::crypto::sign::{ + GenerateParams, SignError, SignRaw, Signature, + }; + use crate::rdata::Dnskey; + use crate::utils::base16; + + //----------- KeyPair ---------------------------------------------------- + + /// A reference to a key pair stored in an [OASIS KMIP] compliant HSM + /// server. + /// + /// [OASIS KMIP]: https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=kmip + #[derive(Clone, Debug)] + pub struct KeyPair { + /// The algorithm used by the key. + algorithm: SecurityAlgorithm, + + /// The KMIP ID of the private key. + private_key_id: String, + + /// The KMIP ID of the public key. + public_key_id: String, + + /// The connection pool for connecting to the KMIP server. + // TODO: Should this be T that impl's a Connection trait, why should + // it know that it's a pool rather than a single connection? + conn_pool: SyncConnPool, + + /// Cached DNSKEY RR for the public key. + dnskey: Dnskey>, + + /// Flags from [`Dnskey`]. + flags: u16, + } + + //--- Constructors + + impl KeyPair { + /// Construct a reference to a KMIP HSM held key pair using key + /// metadata. + pub fn from_metadata( + algorithm: SecurityAlgorithm, + flags: u16, + private_key_id: &str, + public_key_id: &str, + conn_pool: SyncConnPool, + ) -> Result { + let dnskey = PublicKey::for_key_id_and_dnssec_algorithm( + public_key_id, + algorithm, + conn_pool.clone(), + ) + .map_err(|err| GenerateError::Kmip(err.to_string()))? + .dnskey(flags); + + Ok(Self { + algorithm, + private_key_id: private_key_id.to_string(), + public_key_id: public_key_id.to_string(), + conn_pool, + flags, + dnskey, + }) + } + + /// Construct a reference to a KMIP HSM held key pair using key URLs. + pub fn from_urls( + priv_key_url: KeyUrl, + pub_key_url: KeyUrl, + conn_pool: SyncConnPool, + ) -> Result { + if priv_key_url.algorithm() != pub_key_url.algorithm() { + Err(GenerateError::Kmip(format!("Private and public key URLs have different algorithms: {} vs {}", priv_key_url.algorithm(), pub_key_url.algorithm()))) + } else if priv_key_url.flags() != pub_key_url.flags() { + Err(GenerateError::Kmip(format!("Private and public key URLs have different flags: {} vs {}", priv_key_url.flags(), pub_key_url.flags()))) + } else if priv_key_url.server_id() != pub_key_url.server_id() { + Err(GenerateError::Kmip(format!("Private and public key URLs have different server IDs: {} vs {}", priv_key_url.server_id(), pub_key_url.server_id()))) + } else if priv_key_url.server_id() != conn_pool.server_id() { + Err(GenerateError::Kmip(format!("Key URLs have different server ID to the KMIP connection pool: {} vs {}", priv_key_url.server_id(), conn_pool.server_id()))) + } else { + Self::from_metadata( + priv_key_url.algorithm(), + priv_key_url.flags(), + priv_key_url.key_id(), + pub_key_url.key_id(), + conn_pool, + ) + } + } + } + + //--- Accessors + + impl KeyPair { + /// Get the KMIP HSM ID for the private half of this key pair. + pub fn private_key_id(&self) -> &str { + &self.private_key_id + } + + /// Get the KMIP HSM ID for the public half of this key pair. + pub fn public_key_id(&self) -> &str { + &self.public_key_id + } + + /// Get a KMIP URL for the private half of this key pair. + pub fn private_key_url(&self) -> Url { + // + self.mk_key_url(&self.private_key_id).unwrap() + } + + /// Get a KMIP URL for the public half of this key pair. + pub fn public_key_url(&self) -> Url { + self.mk_key_url(&self.public_key_id).unwrap() + } + + /// Get a reference to the KMIP HSM connection pool for this key pair. + pub fn conn_pool(&self) -> &SyncConnPool { + &self.conn_pool + } + } + + //--- Operations + + impl KeyPair { + /// Enqueue a KMIP signing operation using this key pair on the given + /// data. + /// + /// Like [`SignRaw::sign_raw()`] but deferred until + /// [`KeyPair::sign_raw_submit_queue()`] is called. + pub fn sign_raw_enqueue( + &self, + queue: &mut SignQueue, + data: &[u8], + ) -> Result, SignError> { + let request = self.sign_pre(data)?; + let operation = request.operation(); + let batch_item_id = + UniqueBatchItemID(Uuid::new_v4().into_bytes().to_vec()); + let batch_item = + BatchItem(operation, Some(batch_item_id), request); + queue.0.push(batch_item); + Ok(None) + } + + /// Submit the given signing queue as a batch to the KMIP HSM. + // + // TODO: Should the queue store the KMIP connection pool reference and + // should submit() be a method on the queue? + // TODO: What happens if the same queue is used with + // sign_raw_enqueue() but with keys that are held by different KMIP + // HSMs and thus have different KMIP connection pools? + pub fn sign_raw_submit_queue( + &self, + queue: &mut SignQueue, + ) -> Result, SignError> { + // Execute the request and capture the response. + let client = self.conn_pool.get().map_err(|err| { + format!("Error while obtaining KMIP pool connection: {err}") + })?; + + // Drain the queue. + let q_size = queue.0.capacity(); + let mut empty = Vec::with_capacity(q_size); + std::mem::swap(&mut queue.0, &mut empty); + let queue = empty; + + // This will block which could be problematic if executed from an + // async task handler thread as it will block execution of other + // tasks while waiting for the remote KMIP server to respond. + let res = client.do_requests(queue).map_err(|err| { + format!("Error while sending KMIP request: {err}") + })?; + + let mut sigs = Vec::with_capacity(q_size); + for res in res { + let res = res?; + let sig = self.sign_post(res.payload.unwrap())?; + sigs.push(sig); + } + + Ok(sigs) + } + } + + //--- Internal details + + impl KeyPair { + /// Make a KMIP URL for this key using the given KMIP ID. + fn mk_key_url(&self, key_id: &str) -> Result { + // We have to store the algorithm in the URL because the DNSSEC + // algorithm (e.g. 5 and 7) don't necessarily correspond to the + // cryptographic algorithm of the key known to the HSM. And we + // have to store the flags in the URL because these are not known + // to the HSM, they say someting about the use to which the key + // will be put of which the HSM is unaware. + let url = format!( + "kmip://{}/keys/{}?algorithm={}&flags={}", + self.conn_pool.server_id(), + key_id, + self.algorithm, + self.flags + ); + + let url = Url::parse(&url).map_err(|err| { + KeyUrlParseError(format!( + "unable to parse {url} as URL: {err}" + )) + })?; + + Ok(url) + } + + /// Prepare a KMIP signing operation request to sign the given data + /// using this key pair. + fn sign_pre(&self, data: &[u8]) -> Result { + let (crypto_alg, hashing_alg, _digest_type) = match self.algorithm + { + SecurityAlgorithm::RSASHA256 => ( + CryptographicAlgorithm::RSA, + HashingAlgorithm::SHA256, + DigestType::Sha256, + ), + SecurityAlgorithm::ECDSAP256SHA256 => ( + CryptographicAlgorithm::ECDSA, + HashingAlgorithm::SHA256, + DigestType::Sha256, + ), + alg => { + return Err(format!( + "Algorithm not supported for KMIP signing: {alg}" + ) + .into()) + } + }; + let mut cryptographic_parameters = + CryptographicParameters::default() + .with_hashing_algorithm(hashing_alg) + .with_cryptographic_algorithm(crypto_alg); + if self.algorithm == SecurityAlgorithm::RSASHA256 { + cryptographic_parameters = cryptographic_parameters + .with_padding_method(PaddingMethod::PKCS1_v1_5); + } + let request = RequestPayload::Sign( + Some(UniqueIdentifier(self.private_key_id.clone())), + Some(cryptographic_parameters), + Data(data.as_ref().to_vec()), + ); + Ok(request) + } + + /// Process a KMIP HSM signing operation response for this key pair. + fn sign_post( + &self, + res: ResponsePayload, + ) -> Result { + tracing::trace!("Checking sign payload"); + let ResponsePayload::Sign(signed) = res else { + unreachable!(); + }; + + trace!( + "Algorithm: {}, Signature Data: {}", + self.algorithm, + base16::encode_display(&signed.signature_data) + ); + match (self.algorithm, signed.signature_data.len()) { + (SecurityAlgorithm::RSASHA256, _) => Ok(Signature::RsaSha256( + signed.signature_data.into_boxed_slice(), + )), + + (SecurityAlgorithm::ECDSAP256SHA256, _) => { + // ECDSA signature received from Fortanix DSM, decoded + // using this command: + // + // $ echo '' | xxd -r -p | dumpasn1 - + // 0 69: SEQUENCE { + // 2 33: INTEGER + // : 00 C6 A7 D1 2E A1 0C B4 96 BD D9 A5 48 2C 9B F4 + // : 0C EC 9F FC EF 1A 0D 59 BB B9 24 F3 FE DA DC F8 + // : 9E + // 37 32: INTEGER + // : 4B A7 22 69 F2 F8 65 88 63 D0 25 D3 A9 D5 92 4F + // : A2 21 BD 59 CD 27 60 6D 16 C3 79 EF B4 0A CA 33 + // : } + // + // Where the two integer values are known as 'r' and 's'. + let signature = EcdsaSig::from_der(&signed.signature_data).unwrap(); + let mut r = signature.r().to_vec_padded(32).unwrap(); + let mut s = signature.s().to_vec_padded(32).unwrap(); + r.append(&mut s); + Ok(Signature::EcdsaP256Sha256(Box::<[u8; 64]>::new( + r.try_into().unwrap() + ))) + } + + // TODO + //(SecurityAlgorithm::ECDSAP384SHA384, 96) => {}, + //(SecurityAlgorithm::ED25519, 64) => {}, + //(SecurityAlgorithm::ED448, 114) => {}, + + (alg, sig_len) => { + Err(format!("KMIP signature algorithm not supported or signature length incorrect: {sig_len} byte {alg} signature (0x{})", + base16::encode_display(&signed.signature_data + )))? + } + } + } + } + + //----------- SignQueue -------------------------------------------------- + + /// A queue of KMIP signing operations pending batch submission. + #[derive(Debug, Default)] + pub struct SignQueue(Vec); + + impl SignQueue { + /// TODO + pub fn new() -> Self { + Self(vec![]) + } + } + + impl SignRaw for KeyPair { + fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + fn flags(&self) -> u16 { + self.flags + } + + fn dnskey(&self) -> Dnskey> { + self.dnskey.clone() + } + + fn sign_raw(&self, data: &[u8]) -> Result { + let request = self.sign_pre(data)?; + + // Execute the request and capture the response. + let client = self.conn_pool.get().map_err(|err| { + format!("Error while obtaining KMIP pool connection: {err}") + })?; + + // This will block which could be problematic if executed from an + // async task handler thread as it will block execution of other + // tasks while waiting for the remote KMIP server to respond. + let res = client.do_request(request).map_err(|err| { + format!("Error while sending KMIP request: {err}") + })?; + + self.sign_post(res) + } + } + + //----------- generate() ------------------------------------------------- + + /// Generate a new key pair for a given algorithm using a specified HSM. + pub fn generate( + public_key_name: String, + private_key_name: String, + params: GenerateParams, // TODO: Is this enough? Or do we need to take SecurityAlgorithm as input instead of GenerateParams to ensure we don't lose distinctions like 5 vs 7 which are both RSASHA1? + flags: u16, + conn_pool: SyncConnPool, + ) -> Result { + let algorithm = params.algorithm(); + + let client = conn_pool + .get() + .map_err(|err| GenerateError::Kmip(format!("Key generation failed: Cannot connect to KMIP server {}: {err}", conn_pool.server_id())))?; + + // TODO: Determine this on first use of the HSM? + // PyKMIP doesn't support ActivationDate. + // Fortanix DSM does support it and creates the key in an activated + // state but still returns a (harmless?) error: + // Server error: Operation CreateKeyPair failed: Input field `state` + // is not coherent with provided activation/deactivation dates + let activate_on_create = false; + + let use_cryptographic_params = false; + + // Note: Strictly speaking KMIP requires that each key, including + // public and private "halves" of the same key "pair", have a unique + // name within the HSM namespace. We don't enforce that here, e.g. + // maybe you know that your backend is actually a KMIP to PKCS#11 + // gateway and PKCS#11 doesn't have the same restriction and you + // want keys to be named as you are used to with your PKCS#11 HSM. We + // also don't intefere with names by making them unique as that would + // change any max name length calculations performed by the caller + // to avoid known issues with backend name limitations for their + // particular HSM (the PKCS#11 and KMIP specifications are silent on + // name limits but implementations definitely have limits, and not all + // the same). + + let mut common_attrs = vec![]; + let priv_key_attrs = vec![ + // Krill supplies a name at creation time. Do we need to? + // Note: Fortanix DSM requires a name for at least the private + // key. + request::Attribute::Name(private_key_name), + request::Attribute::CryptographicUsageMask( + CryptographicUsageMask::Sign, + ), + ]; + let pub_key_attrs = vec![ + // Krill supplies a name at creation time. Do we need to? + // Note: Fortanix DSM requires a name for at least the private + // key. + request::Attribute::Name(public_key_name), + // Krill does verification, do we need to? ODS doesn't. + // Note: PyKMIP requires a Cryptographic Usage Mask for the public + // key. + request::Attribute::CryptographicUsageMask( + CryptographicUsageMask::Verify, + ), + ]; + + // PyKMIP doesn't support CryptographicParameters so we cannot supply + // HashingAlgorithm. It also doesn't support the Hash operation. + // How do we specify SHA256 hashing? Do we have to do it ourselves + // post-signing? Can we just specify the hashing to do when invoking + // the Sign operation? + // Fortanix DSM also doesn't support Cryptographic Parameters: + // Server error: Operation CreateKeyPair failed: Don't have handling + // for attribute Cryptographic Parameters + + // PyKMIP doesn't support Attribute::ActivationDate. For HSMs that + // don't support it we have to do a separate Activate operation after + // creating the key pair. + // Fortanix DSM does support ActivationDate. + + match params { + GenerateParams::RsaSha256 { bits } => { + // RFC 8624 3.1 DNSSEC Signing: MUST + // https://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html#_Toc395776503 + // "For RSA, Cryptographic Length corresponds to the bit + // length of the Modulus" + + // https://www.rfc-editor.org/rfc/rfc5702.html#section-2.1 + // 2.1. RSA/SHA-256 DNSKEY Resource Records + // "For interoperability, as in [RFC3110], the key size of + // RSA/SHA-256 keys MUST NOT be less than 512 bits and MUST + // NOT be more than 4096 bits." + if !(512..=4096).contains(&bits) { + return Err(GenerateError::UnsupportedAlgorithm); + } + + if use_cryptographic_params { + common_attrs.push( + request::Attribute::CryptographicParameters( + CryptographicParameters::default() + .with_digital_signature_algorithm(DigitalSignatureAlgorithm::SHA256WithRSAEncryption_PKCS1_v1_5) + ) + ) + } else { + common_attrs.push( + request::Attribute::CryptographicAlgorithm( + CryptographicAlgorithm::RSA, + ), + ); + common_attrs.push( + request::Attribute::CryptographicLength( + bits.try_into().unwrap(), + ), + ); + } + } + GenerateParams::RsaSha512 { .. } => { + return Err(GenerateError::UnsupportedAlgorithm); + } + GenerateParams::EcdsaP256Sha256 => { + // PyKMIP doesn't support ECDSA: + // "Operation CreateKeyPair failed: The cryptographic + // algorithm (CryptographicAlgorithm.ECDSA) is not a + // supported asymmetric key algorithm." + + if use_cryptographic_params { + common_attrs.push( + request::Attribute::CryptographicParameters( + CryptographicParameters::default() + .with_digital_signature_algorithm( + DigitalSignatureAlgorithm::ECDSAWithSHA256, + ), + ), + ) + } else { + // RFC 8624 3.1 DNSSEC Signing: MUST + // https://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html#_Toc395776503 + // "For ECDSA, ECDH, and ECMQV algorithms, Cryptographic + // Length corresponds to the bit length of parameter + // Q." + common_attrs.push( + request::Attribute::CryptographicAlgorithm( + CryptographicAlgorithm::ECDSA, + ), + ); + // ODS doesn't tell PKCS#11 a Q length. I have no idea + // what value we should put here, but as Q length is + // optional let's try not passing it. + // Note: PyKMIP requires a length: use 256 from P-256? + // Note: Fortanix also requires a length and gives error + // "missing required field `elliptic_curve` in request + // body" if cryptographic length is not specified, and + // a value of 256 works fine while a value of 255 causes + // error "Unsupported length for ECC key". When using 256 + // the Fortanix UI shows the key as type EC with curve + // NistP256 so that seems good. + common_attrs + .push(request::Attribute::CryptographicLength(256)); + } + } + GenerateParams::EcdsaP384Sha384 => { + // RFC 8624 3.1 DNSSEC Signing: MAY + // TODO + return Err(GenerateError::UnsupportedAlgorithm); + } + GenerateParams::Ed25519 => { + // RFC 8624 3.1 DNSSEC Signing: RECOMMENDED + // TODO + return Err(GenerateError::UnsupportedAlgorithm); + } + GenerateParams::Ed448 => { + // RFC 8624 3.1 DNSSEC Signing: MAY + // TODO + return Err(GenerateError::UnsupportedAlgorithm); + } + }; + + if activate_on_create { + // https://docs.oasis-open.org/kmip/testcases/v1.1/kmip-testcases-v1.1.html + // shows an example including an Activation Date value of 2 noted + // as meaning Thu Jan 01 01:00:02 CET 1970. i.e. the activation + // date should be a UNIX epoch timestamp. + let time_now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + common_attrs.push(request::Attribute::ActivationDate(time_now)); + } + + let request = RequestPayload::CreateKeyPair( + Some(CommonTemplateAttribute::new(common_attrs)), + Some(PrivateKeyTemplateAttribute::new(priv_key_attrs)), + Some(PublicKeyTemplateAttribute::new(pub_key_attrs)), + ); + + // Execute the request and capture the response + let response = client.do_request(request).map_err(|err| { + error!("KMIP request failed: {err}"); + debug!( + "KMIP last request: {}", + client.last_req_diag_str().unwrap_or_default() + ); + debug!( + "KMIP last response: {}", + client.last_res_diag_str().unwrap_or_default() + ); + GenerateError::Kmip(err.to_string()) + })?; + tracing::trace!("Key generation operation complete"); + + // Drop the KMIP client so that it will be returned to the pool and + // thus be available below when KeyPair::new() is invoked and tries to + // fetch the details needed to determine the DNSKEY RR. + drop(client); + + // Process the successful response + let ResponsePayload::CreateKeyPair(payload) = response else { + error!("KMIP request failed: Wrong response type received!"); + return Err(GenerateError::Kmip("Unable to parse KMIP response: payload should be CreateKeyPair".to_string())); + }; + + let CreateKeyPairResponsePayload { + private_key_unique_identifier, + public_key_unique_identifier, + } = payload; + + tracing::trace!("Creating KeyPair with DNSKEY"); + + let key_pair = KeyPair::from_metadata( + algorithm, + flags, + private_key_unique_identifier.as_str(), + public_key_unique_identifier.as_str(), + conn_pool.clone(), + ) + .map_err(|err| GenerateError::Kmip(err.to_string()))?; + + // Activate the key if not already, otherwise it cannot be used for + // signing. + if !activate_on_create { + let client = conn_pool + .get() + .map_err(|err| GenerateError::Kmip(format!("Key generation failed: Cannot connect to KMIP server {}: {err}", conn_pool.server_id())))?; + let request = + RequestPayload::Activate(Some(private_key_unique_identifier)); + + // Execute the request and capture the response + tracing::trace!("Activating KMIP key..."); + let response = client.do_request(request).map_err(|err| { + eprintln!("KMIP activate private key request failed: {err}"); + eprintln!( + "KMIP last request: {}", + client.last_req_diag_str().unwrap_or_default() + ); + eprintln!( + "KMIP last response: {}", + client.last_res_diag_str().unwrap_or_default() + ); + GenerateError::Kmip(err.to_string()) + })?; + tracing::trace!("Activate operation complete"); + + // Process the successful response + let ResponsePayload::Activate(_) = response else { + error!("KMIP request failed: Wrong response type received!"); + return Err(GenerateError::Kmip("Unable to parse KMIP response: payload should be Activate".to_string())); + }; + } + + Ok(key_pair) + } + + //----------- destroy() -------------------------------------------------- + + /// Destroy a KMIP key by ID. + /// + /// Note: A KMIP key cannot be destroyed if it is active. To deactivate + /// the key we must first "revoke" it. + pub fn destroy( + key_id: &str, + conn_pool: SyncConnPool, + ) -> Result<(), DestroyError> { + let client = conn_pool + .get() + .map_err(|err| DestroyError::Kmip(format!("Key destruction failed: Cannot connect to KMIP server {}: {err}", conn_pool.server_id())))?; + + client + .revoke_key(key_id) + .map_err(|err| DestroyError::Kmip(err.to_string()))?; + client + .destroy_key(key_id) + .map_err(|err| DestroyError::Kmip(err.to_string())) + } +} + +//============ Error Types =================================================== + +//--- Conversion + +impl From for SignError { + fn from(err: kmip::client::Error) -> Self { + err.to_string().into() + } +} + +//----------- GenerateError -------------------------------------------------- + +/// An error occurred while generating a key pair with a KMIP server. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm is not supported. + UnsupportedAlgorithm, + + /// A problem occurred while communicating with the KMIP server. + Kmip(String), +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedAlgorithm => { + write!(f, "algorithm not supported") + } + Self::Kmip(err) => { + write!(f, "a problem occurred while communicating with the KMIP server: {err}") + } + } + } +} + +//--- impl Error + +impl std::error::Error for GenerateError {} + +//------------ DestroyError -------------------------------------------------- + +/// An error while destroying a key using KMIP. +#[derive(Clone, Debug)] +pub enum DestroyError { + /// A problem occurred while communicating with the KMIP server. + Kmip(String), +} + +//--- Formatting + +impl fmt::Display for DestroyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Kmip(err) => { + write!(f, "a problem occurred while communicating with the KMIP server: {err}") + } + } + } +} + +//--- Error + +impl std::error::Error for DestroyError {} + +//------------ KeyUrlError --------------------------------------------------- + +/// An error occurred while parsing a KMIP key URL. +#[derive(Clone, Debug)] +pub struct KeyUrlParseError(String); + +//--- Formatting + +impl fmt::Display for KeyUrlParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid key URL: {}", self.0) + } +} + +//--- impl Error + +impl std::error::Error for KeyUrlParseError {} + +//--- Conversions + +impl From for KeyUrlParseError { + fn from(err: String) -> Self { + KeyUrlParseError(err) + } +} + +//------------ PublicKeyError ------------------------------------------------ + +/// An error occurred while retrieving a KMIP public key. +#[derive(Clone, Debug)] +pub enum PublicKeyError { + /// The cryptographic algorithm of the KMIP key does not match the + /// specified DNSSEC algorithm. + AlgorithmMismatch { + /// The DNSSEC algorithm that was expected. + expected: SecurityAlgorithm, + + /// The type of key data received from the KMIP server. + actual: String, + }, + + /// A problem occurred while communicating with the KMIP server. + Kmip(String), +} + +//--- Formatting + +impl fmt::Display for PublicKeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AlgorithmMismatch { expected, actual } => { + write!(f, "algorithm mismatch: expected {expected} but found {actual}") + } + Self::Kmip(err) => { + write!(f, "a problem occurred while communicating with the KMIP server: {err}") + } + } + } +} + +//--- impl Error + +impl std::error::Error for PublicKeyError {} + +//--- Conversions + +impl From for PublicKeyError { + fn from(err: kmip::client::Error) -> Self { + PublicKeyError::Kmip(err.to_string()) + } +} + +//============ Testing ======================================================= + +#[cfg(test)] +mod tests { + use core::time::Duration; + + use std::fs::File; + use std::io::{BufReader, Read}; + use std::string::ToString; + use std::time::SystemTime; + use std::vec::Vec; + + use kmip::client::pool::ConnectionManager; + use kmip::client::ConnectionSettings; + + use crate::crypto::kmip::sign::generate; + use crate::crypto::sign::SignRaw; + use crate::logging::init_logging; + + #[test] + #[ignore = "Requires running PyKMIP"] + fn pykmip_connect() { + init_logging(); + let mut cert_bytes = Vec::new(); + let file = File::open( + "/home/ximon/docker_data/pykmip/pykmip-data/selfsigned.crt", + ) + .unwrap(); + let mut reader = BufReader::new(file); + reader.read_to_end(&mut cert_bytes).unwrap(); + + let mut key_bytes = Vec::new(); + let file = File::open( + "/home/ximon/docker_data/pykmip/pykmip-data/selfsigned.key", + ) + .unwrap(); + let mut reader = BufReader::new(file); + reader.read_to_end(&mut key_bytes).unwrap(); + + let conn_settings = ConnectionSettings { + host: "localhost".to_string(), + port: 5696, + insecure: true, + client_cert: Some(kmip::client::ClientCertificate::SeparatePem { + cert_bytes, + key_bytes, + }), + ..Default::default() + }; + + eprintln!("Creating pool..."); + let pool = ConnectionManager::create_connection_pool( + "Test server".to_string(), + conn_settings.into(), + 16384, + Some(Duration::from_secs(60)), + Some(Duration::from_secs(60)), + ) + .unwrap(); + + eprintln!("Connecting..."); + let client = pool.get().unwrap(); + + eprintln!("Connected"); + let res = client.query(); + dbg!(&res); + res.unwrap(); + + let pub_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let pri_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let res = generate( + pub_key_name, + pri_key_name, + crate::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, + // crate::crypto::sign::GenerateParams::EcdsaP256Sha256, + 256, + pool, + ); + dbg!(&res); + let key = res.unwrap(); + + eprintln!("DNSKEY: {}", key.dnskey()); + } + + #[test] + #[ignore = "Requires Fortanix credentials"] + fn fortanix_dsm_test() { + // Note: keyls fails against Fortanix DSM for some reason with error: + // Error: Server error: Operation Locate failed: expected + // AttributeValue, got ObjectType, Diagnostics: + // req: 78[77[69[6Ai6Bi]0C[23[24e1:25[99tA1t]]]0Di]0F[5Ce8:79[08[0At57e4:]]]], + // resp: 7B[7A[69[6Ai6Bi]92d0Di]0F[5Ce8:7Fe1:7Ee100:7Dt]] + + init_logging(); + + // conn_settings.host = "eu.smartkey.io".to_string(); + // conn_settings.port = 5696; + // conn_settings.username = Some(env!("FORTANIX_USER").to_string()); + // conn_settings.password = Some(env!("FORTANIX_PASS").to_string()); + + let conn_settings = ConnectionSettings { + host: "127.0.0.1".to_string(), + port: 5696, + insecure: true, + connect_timeout: Some(Duration::from_secs(3)), + read_timeout: Some(Duration::from_secs(30)), + write_timeout: Some(Duration::from_secs(3)), + ..Default::default() + }; + + eprintln!("Creating pool..."); + let pool = ConnectionManager::create_connection_pool( + "Test server".to_string(), + conn_settings.into(), + 16384, + Some(Duration::from_secs(60)), + Some(Duration::from_secs(60)), + ) + .unwrap(); + + eprintln!("Connecting..."); + let client = pool.get().unwrap(); + + eprintln!("Connected"); + // let res = client.query(); + // dbg!(&res); + // res.unwrap(); + + let pub_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let pri_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let res = generate( + pub_key_name, + pri_key_name, + crate::crypto::sign::GenerateParams::RsaSha256 { bits: 1024 }, + // crate::crypto::sign::GenerateParams::EcdsaP256Sha256, + 256, + pool, + ); + let key = res.unwrap(); + eprintln!("Generated public key with id: {}", key.public_key_id()); + eprintln!("Generated private key with id: {}", key.private_key_id()); + + // sleep(Duration::from_secs(5)); + + eprintln!("DNSKEY: {}", key.dnskey()); + + client.activate_key(key.public_key_id()).unwrap(); + + // Fortanix: Activating the public key also activates the private key. + // Attempting to then activate the private key fails as it is already + // active. Yet signing fails with "Object is not yet active"... + // client.activate_key(key.private_key_id()).unwrap(); + + // // This works round the not yet active yet error. + // sleep(Duration::from_secs(5)); + + // let request = RequestPayload::Sign( + // Some(UniqueIdentifier(key.private_key_id().to_string())), + // // While the KMIP 1.2 spec says crypto parameters are optional and + // // if not specified those of the key will be used, Fortanix + // // complains about "No cryptographic parameters specified" if this + // // is None, and "Must specicify HashingAlgorithm" if that is not + // // specified. + // Some( + // CryptographicParameters::default() + // // .with_padding_method(PaddingMethod::) + // .with_hashing_algorithm(HashingAlgorithm::SHA256) + // .with_cryptographic_algorithm( + // CryptographicAlgorithm::RSA, + // //CryptographicAlgorithm::ECDSA, + // ), + // ), + // Data("Message for ECDSA signing".as_bytes().to_vec()), + // ); + + // // Execute the request and capture the response + // let res = client.do_request(request).unwrap(); + + // dbg!(&res); + + // let ResponsePayload::Sign(signed) = res else { + // unreachable!(); + // }; + + // // let signature = + // // openssl::ecdsa::EcdsaSig::from_der(&signed.signature_data) + // // .unwrap(); + + // // dbg!(signature.r().to_vec_padded(32)); + // // dbg!(signature.s().to_vec_padded(32)); + + // // dbg!(response); + } +} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index a35077d74..d3842fb37 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -5,15 +5,25 @@ //! private key operations such as generation and signing. All features of //! this module are enabled with the `unstable-crypto-sign` feature flag. //! -//! 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 +//! This crate supports OpenSSL and Ring backends for local in-memory +//! cryptography, and also an [OASIS KMIP 1.2] backend for generating, and +//! signing with, remote key pairs using compliant HSMs. +//! +//! These cryptographic backends are gated on the `openssl`, `ring` and +//! `kmip` features, respectively. They offer mostly equivalent functionality +//! but OpenSSL supports a larger set of signing algorithms than Ring (and, +//! or RSA keys, supports weaker key sizes). +//! +//! An implementation agnostic #![cfg_attr(feature = "unstable-crypto-sign", doc = "[`sign`]")] #![cfg_attr(not(feature = "unstable-crypto-sign"), doc = "`sign`")] -//! backend is provided for users that wish -//! to use either or both backends at runtime. +//! backend is provided for users that wish to use either or both of the +//! OpenSSL or Ring backends at runtime. The KMIP backend must be used +//! explicitly as it requires details of the KMIP HSM server to connect to +//! and so has a slightly different interface to that of the OpenSSL and +//! Ring backends. +//! +//! [OASIS KMIP 1.2]: https://docs.oasis-open.org/kmip/spec/v1.2/kmip-spec-v1.2.html //! //! Each backend module ( #![cfg_attr( @@ -33,6 +43,15 @@ not(all(feature = "unstable-crypto-sign", feature = "ring")), doc = "`ring::sign`" )] +//! , +#![cfg_attr( + all(feature = "unstable-crypto-sign", feature = "kmip"), + doc = "[`kmip::sign`]" +)] +#![cfg_attr( + not(all(feature = "unstable-crypto-sign", feature = "kmip")), + doc = "`kmip::sign`" +)] //! , and #![cfg_attr(feature = "unstable-crypto-sign", doc = "[`sign`]")] #![cfg_attr(not(feature = "unstable-crypto-sign"), doc = "`sign`")] @@ -73,22 +92,22 @@ //! //! In addition to private key operations, this module provides the #![cfg_attr( - any(feature = "ring", feature = "openssl"), + any(feature = "ring", feature = "openssl", feature = "kmip"), doc = "[`common::PublicKey`]" )] #![cfg_attr( - not(any(feature = "ring", feature = "openssl")), + not(any(feature = "ring", feature = "openssl", feature = "kmip")), doc = "`common::PublicKey`" )] //! type for signature verification. //! //! The module also support computing message digests using the #![cfg_attr( - any(feature = "ring", feature = "openssl"), + any(feature = "ring", feature = "openssl", feature = "kmip"), doc = "[`common::DigestBuilder`]" )] #![cfg_attr( - not(any(feature = "ring", feature = "openssl")), + not(any(feature = "ring", feature = "openssl", feature = "kmip")), doc = "`common::DigestBuilder`" )] //! type. @@ -128,6 +147,7 @@ #![warn(clippy::missing_docs_in_private_items)] pub mod common; +pub mod kmip; pub mod openssl; pub mod ring; pub mod sign; diff --git a/src/crypto/openssl.rs b/src/crypto/openssl.rs index 907c2bb69..6251c6b5e 100644 --- a/src/crypto/openssl.rs +++ b/src/crypto/openssl.rs @@ -685,6 +685,10 @@ pub mod sign { self.algorithm } + fn flags(&self) -> u16 { + self.flags + } + fn dnskey(&self) -> Dnskey> { match self.algorithm { SecurityAlgorithm::RSASHA256 => { @@ -749,7 +753,7 @@ pub mod sign { let signature = self .sign(data) .map(Vec::into_boxed_slice) - .map_err(|_| SignError)?; + .map_err(|err| format!("OpenSSL signing failed: {err}"))?; match self.algorithm { SecurityAlgorithm::RSASHA256 => { @@ -759,22 +763,32 @@ pub mod sign { SecurityAlgorithm::ECDSAP256SHA256 => signature .try_into() .map(Signature::EcdsaP256Sha256) - .map_err(|_| SignError), + .map_err(|_| { + "OpenSSL ECDSAP256SHA256 signature too large".into() + }), + SecurityAlgorithm::ECDSAP384SHA384 => signature .try_into() .map(Signature::EcdsaP384Sha384) - .map_err(|_| SignError), + .map_err(|_| { + "OpenSSL ECDSAP384SHA384 signature too large".into() + }), + + SecurityAlgorithm::ED25519 => { + signature.try_into().map(Signature::Ed25519).map_err( + |_| "OpenSSL ED25519 signature too large".into(), + ) + } - SecurityAlgorithm::ED25519 => signature - .try_into() - .map(Signature::Ed25519) - .map_err(|_| SignError), SecurityAlgorithm::ED448 => signature .try_into() .map(Signature::Ed448) - .map_err(|_| SignError), + .map_err(|_| "OpenSSL ED448 signature too large".into()), - _ => unreachable!(), + alg => Err(format!( + "OpenSSL signature algorithm not supported: {alg}" + ) + .into()), } } } diff --git a/src/crypto/ring.rs b/src/crypto/ring.rs index c3a7fb048..2e5156644 100644 --- a/src/crypto/ring.rs +++ b/src/crypto/ring.rs @@ -502,6 +502,15 @@ pub mod sign { } } + fn flags(&self) -> u16 { + match *self { + KeyPair::RsaSha256 { flags, .. } => flags, + KeyPair::EcdsaP256Sha256 { flags, .. } => flags, + KeyPair::EcdsaP384Sha384 { flags, .. } => flags, + KeyPair::Ed25519(_, flags) => flags, + } + } + fn dnskey(&self) -> Dnskey> { match self { Self::RsaSha256 { key, flags, rng: _ } => { @@ -575,35 +584,41 @@ pub mod sign { .map(|()| { Signature::RsaSha256(buf.into_boxed_slice()) }) - .map_err(|_| SignError) + .map_err(|_| "Ring RSASHA256 signing failed".into()) } Self::EcdsaP256Sha256 { key, flags: _, rng } => key .sign(&**rng, data) .map(|sig| Box::<[u8]>::from(sig.as_ref())) - .map_err(|_| SignError) + .map_err(|_| "Ring ECDSAP256SHA256 signing failed".into()) .and_then(|buf| { buf.try_into() .map(Signature::EcdsaP256Sha256) - .map_err(|_| SignError) + .map_err(|_| { + "Ring ECDSAP256SHA256 signature too large" + .into() + }) }), Self::EcdsaP384Sha384 { key, flags: _, rng } => key .sign(&**rng, data) .map(|sig| Box::<[u8]>::from(sig.as_ref())) - .map_err(|_| SignError) + .map_err(|_| "Ring ECDSAP384SHA384 signing failed".into()) .and_then(|buf| { buf.try_into() .map(Signature::EcdsaP384Sha384) - .map_err(|_| SignError) + .map_err(|_| { + "Ring ECDSAP384SHA384 signature too large" + .into() + }) }), 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) + buf.try_into().map(Signature::Ed25519).map_err(|_| { + "Ring ED25519 signature too large".into() + }) } } } diff --git a/src/crypto/sign.rs b/src/crypto/sign.rs index 9a8a80411..8ba573854 100644 --- a/src/crypto/sign.rs +++ b/src/crypto/sign.rs @@ -81,6 +81,7 @@ use std::boxed::Box; use std::fmt; +use std::string::{String, ToString}; use std::vec::Vec; use secrecy::{ExposeSecret, SecretBox}; @@ -95,6 +96,9 @@ use super::openssl; #[cfg(feature = "ring")] use super::ring; +#[cfg(feature = "kmip")] +use super::kmip; + //----------- GenerateParams ------------------------------------------------- /// Parameters for generating a secret key. @@ -172,6 +176,9 @@ pub trait SignRaw { /// [RFC 8624, section 3.1]: https://datatracker.ietf.org/doc/html/rfc8624#section-3.1 fn algorithm(&self) -> SecurityAlgorithm; + /// TODO + fn flags(&self) -> u16; + /// The public key. /// /// This can be used to verify produced signatures. It must use the same @@ -310,6 +317,10 @@ pub enum KeyPair { /// A key backed by OpenSSL. #[cfg(feature = "openssl")] OpenSSL(openssl::sign::KeyPair), + + /// A key backed by a KMIP capable HSM. + #[cfg(feature = "kmip")] + Kmip(kmip::sign::KeyPair), } //--- Conversion to and from bytes @@ -366,6 +377,19 @@ impl SignRaw for KeyPair { Self::Ring(key) => key.algorithm(), #[cfg(feature = "openssl")] Self::OpenSSL(key) => key.algorithm(), + #[cfg(feature = "kmip")] + Self::Kmip(key) => key.algorithm(), + } + } + + fn flags(&self) -> u16 { + match self { + #[cfg(feature = "ring")] + Self::Ring(key) => key.flags(), + #[cfg(feature = "openssl")] + Self::OpenSSL(key) => key.flags(), + #[cfg(feature = "kmip")] + Self::Kmip(key) => key.flags(), } } @@ -375,6 +399,8 @@ impl SignRaw for KeyPair { Self::Ring(key) => key.dnskey(), #[cfg(feature = "openssl")] Self::OpenSSL(key) => key.dnskey(), + #[cfg(feature = "kmip")] + Self::Kmip(key) => key.dnskey(), } } @@ -384,6 +410,8 @@ impl SignRaw for KeyPair { Self::Ring(key) => key.sign_raw(data), #[cfg(feature = "openssl")] Self::OpenSSL(key) => key.sign_raw(data), + #[cfg(feature = "kmip")] + Self::Kmip(key) => key.sign_raw(data), } } } @@ -1016,17 +1044,29 @@ impl std::error::Error for GenerateError {} /// 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(Copy, Clone, Debug, PartialEq, Eq)] -pub struct SignError; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignError(String); impl fmt::Display for SignError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("could not create a cryptographic signature") + write!(f, "could not create a cryptographic signature: {}", self.0) } } impl std::error::Error for SignError {} +impl From for SignError { + fn from(err: String) -> Self { + SignError(err) + } +} + +impl From<&'static str> for SignError { + fn from(err: &'static str) -> Self { + SignError(err.to_string()) + } +} + //----------- BindFormatError ------------------------------------------------ /// An error in loading a [`SecretKeyBytes`] from the conventional DNS format. diff --git a/src/dep.rs b/src/dep.rs index db6c635b1..c5bc76cd6 100644 --- a/src/dep.rs +++ b/src/dep.rs @@ -1,3 +1,6 @@ //! Re-exports of dependencies pub use octseq; + +#[cfg(feature = "kmip")] +pub use kmip; diff --git a/src/dnssec/sign/error.rs b/src/dnssec/sign/error.rs index 1a0791000..7dc2fc11a 100644 --- a/src/dnssec/sign/error.rs +++ b/src/dnssec/sign/error.rs @@ -7,7 +7,7 @@ use crate::rdata::dnssec::Timestamp; //------------ SigningError -------------------------------------------------- -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] pub enum SigningError { /// One or more keys does not have a signature validity period defined. NoSignatureValidityPeriodProvided, diff --git a/src/dnssec/sign/records.rs b/src/dnssec/sign/records.rs index d1beda939..c7fa53869 100644 --- a/src/dnssec/sign/records.rs +++ b/src/dnssec/sign/records.rs @@ -388,6 +388,10 @@ impl<'a, N, D> OwnerRrs<'a, N, D> { OwnerRrs { slice } } + pub fn into_inner(self) -> &'a [Record] { + self.slice + } + pub fn owner(&self) -> &N { self.slice[0].owner() } diff --git a/src/dnssec/sign/signatures/rrsigs.rs b/src/dnssec/sign/signatures/rrsigs.rs index 27aeb986f..281d907ac 100644 --- a/src/dnssec/sign/signatures/rrsigs.rs +++ b/src/dnssec/sign/signatures/rrsigs.rs @@ -3,6 +3,7 @@ use core::convert::{AsRef, From}; use core::fmt::Display; use core::marker::Send; +use core::slice; use std::boxed::Box; use std::cmp::Ordering; use std::fmt::Debug; @@ -13,17 +14,17 @@ use octseq::{OctetsFrom, OctetsInto}; use tracing::debug; use crate::base::cmp::CanonicalOrd; -use crate::base::iana::Rtype; +use crate::base::iana::{Class, Rtype}; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; -use crate::base::Name; -use crate::crypto::sign::SignRaw; +use crate::base::{Name, Ttl}; +use crate::crypto::sign::{SignRaw, Signature}; use crate::dnssec::sign::error::SigningError; use crate::dnssec::sign::keys::signingkey::SigningKey; use crate::dnssec::sign::records::{RecordsIter, Rrset}; use crate::rdata::dnssec::{ProtoRrsig, Timestamp}; -use crate::rdata::{Rrsig, ZoneRecordData}; +use crate::rdata::Rrsig; //------------ GenerateRrsigConfig ------------------------------------------- @@ -95,9 +96,9 @@ impl GenerateRrsigConfig { /// [RFC 9364]: https://www.rfc-editor.org/rfc/rfc9364 // TODO: Add mutable iterator based variant. #[allow(clippy::type_complexity)] -pub fn sign_sorted_zone_records( +pub fn sign_sorted_zone_records( apex_owner: &N, - mut records: RecordsIter<'_, N, ZoneRecordData>, + records: RecordsIter<'_, N, D>, keys: &[&SigningKey], config: &GenerateRrsigConfig, ) -> Result>>, SigningError> @@ -119,6 +120,55 @@ where + Clone + FromBuilder + From<&'static [u8]>, + D: CanonicalOrd + ComposeRecordData, +{ + sign_sorted_zone_records_with( + apex_owner, + records, + keys, + config, + sign_sorted_rrset_in::, + ) +} + +#[allow(clippy::type_complexity)] +pub fn sign_sorted_zone_records_with<'a, 'b, N, Octs, D, Inner, O, F>( + apex_owner: &N, + mut records: RecordsIter<'b, N, D>, + keys: &[&'a SigningKey], + config: &GenerateRrsigConfig, + signer_fn: F, +) -> Result, SigningError> +where + Inner: Debug + SignRaw, + N: ToName + + PartialEq + + Clone + + Debug + + Display + + Send + + CanonicalOrd + + From>, + Octs: AsRef<[u8]> + + Debug + + From> + + Send + + OctetsFrom> + + Clone + + FromBuilder + + From<&'static [u8]>, + D: RecordData, + F: Fn( + &'a SigningKey, + Rtype, + Class, + N, + Ttl, + slice::Iter<'b, Record>, + Timestamp, + Timestamp, + &mut Vec, + ) -> Result, { // The generated collection of RRSIG RRs that will be returned to the // caller. @@ -190,9 +240,13 @@ where for key in keys { let inception = config.inception; let expiration = config.expiration; - let rrsig_rr = sign_sorted_rrset_in( + let rrsig_rr = signer_fn( key, - &rrset, + rrset.rtype(), + rrset.class(), + rrset.owner().clone(), + rrset.ttl(), + rrset.iter(), inception, expiration, &mut reusable_scratch, @@ -232,10 +286,10 @@ pub fn sign_rrset( expiration: Timestamp, ) -> Result>, SigningError> where - N: ToName + Debug + Clone + From>, - D: Clone + Debug + RecordData + ComposeRecordData + CanonicalOrd, + N: ToName + Debug + Clone + From> + CanonicalOrd, Inner: Debug + SignRaw, Octs: AsRef<[u8]> + Clone + Debug + OctetsFrom>, + D: CanonicalOrd + ComposeRecordData + Clone, { let mut records = rrset.as_slice().to_vec(); records @@ -243,7 +297,17 @@ where let rrset = Rrset::new(&records) .expect("records is not empty so new should not fail"); - sign_sorted_rrset_in(key, &rrset, inception, expiration, &mut vec![]) + sign_sorted_rrset_in( + key, + rrset.rtype(), + rrset.class(), + rrset.owner().clone(), + rrset.ttl(), + rrset.iter(), + inception, + expiration, + &mut vec![], + ) } /// Generate `RRSIG` records for a given RRset. @@ -269,24 +333,96 @@ where /// https://www.rfc-editor.org/rfc/rfc4035.html#section-2.2 /// [RFC 6840 section 5.11]: /// https://www.rfc-editor.org/rfc/rfc6840.html#section-5.11 -pub fn sign_sorted_rrset_in( - key: &SigningKey, - rrset: &Rrset<'_, N, D>, +#[allow(clippy::too_many_arguments)] +pub fn sign_sorted_rrset_in<'a, 'b, N, Octs, D, Inner>( + key: &'a SigningKey, + rrset_rtype: Rtype, + rrset_class: Class, + rrset_owner: N, + rrset_ttl: Ttl, + rrset_iter: slice::Iter<'b, Record>, inception: Timestamp, expiration: Timestamp, scratch: &mut Vec, ) -> Result>, SigningError> where N: ToName + Clone + Debug + From>, - D: RecordData + Debug + ComposeRecordData + CanonicalOrd, Inner: Debug + SignRaw, Octs: AsRef<[u8]> + Clone + Debug + OctetsFrom>, + D: CanonicalOrd + ComposeRecordData, +{ + let rrsig = sign_sorted_rrset_in_pre( + key, + rrset_rtype, + rrset_owner.rrsig_label_count(), + rrset_ttl, + rrset_iter, + inception, + expiration, + scratch, + )?; + let signature = key.raw_secret_key().sign_raw(&*scratch)?; + sign_sorted_rrset_in_post( + signature, + rrsig, + rrset_owner, + rrset_class, + rrset_ttl, + ) +} + +pub fn sign_sorted_rrset_in_post( + signature: Signature, + rrsig: ProtoRrsig, + rrset_owner: N, + rrset_class: Class, + rrset_ttl: Ttl, +) -> Result>, SigningError> +where + N: ToName + Clone + Debug + From>, + Octs: AsRef<[u8]> + Clone + Debug + OctetsFrom>, +{ + let signature = signature.as_ref().to_vec(); + let Ok(signature) = signature.try_octets_into() else { + return Err(SigningError::OutOfMemory); + }; + + let rrsig = rrsig.into_rrsig(signature).expect("long signature"); + + // RFC 4034 + // 3.1.3. The Labels Field + // ... + // "The value of the Labels field MUST be less than or equal to the + // number of labels in the RRSIG owner name." + debug_assert!( + (rrsig.labels() as usize) < rrset_owner.iter_labels().count() + ); + + Ok(Record::new(rrset_owner, rrset_class, rrset_ttl, rrsig)) +} + +#[allow(clippy::too_many_arguments)] +pub fn sign_sorted_rrset_in_pre( + key: &SigningKey, + rrset_rtype: Rtype, + rrset_owner_rrsig_label_count: u8, + rrset_ttl: Ttl, + rrset_iter: slice::Iter<'_, Record>, + inception: Timestamp, + expiration: Timestamp, + scratch: &mut Vec, +) -> Result, SigningError> +where + N: ToName + Clone + Debug + From>, + Inner: Debug + SignRaw, + Octs: AsRef<[u8]> + Clone + Debug + OctetsFrom>, + D: CanonicalOrd + ComposeRecordData, { // RFC 4035 // 2.2. Including RRSIG RRs in a Zone // ... // "An RRSIG RR itself MUST NOT be signed" - if rrset.rtype() == Rtype::RRSIG { + if rrset_rtype == Rtype::RRSIG { return Err(SigningError::RrsigRrsMustNotBeSigned); } @@ -304,10 +440,10 @@ where // the same owner name will have different TTL values if the RRsets // they cover have different TTL values." let rrsig = ProtoRrsig::new( - rrset.rtype(), + rrset_rtype, key.algorithm(), - rrset.owner().rrsig_label_count(), - rrset.ttl(), + rrset_owner_rrsig_label_count, + rrset_ttl, expiration, inception, key.dnskey().key_tag(), @@ -329,32 +465,11 @@ where scratch.clear(); rrsig.compose_canonical(scratch).unwrap(); - for record in rrset.iter() { + for record in rrset_iter { record.compose_canonical(scratch).unwrap(); } - let signature = key.raw_secret_key().sign_raw(&*scratch)?; - let signature = signature.as_ref().to_vec(); - let Ok(signature) = signature.try_octets_into() else { - return Err(SigningError::OutOfMemory); - }; - - let rrsig = rrsig.into_rrsig(signature).expect("long signature"); - - // RFC 4034 - // 3.1.3. The Labels Field - // ... - // "The value of the Labels field MUST be less than or equal to the - // number of labels in the RRSIG owner name." - debug_assert!( - (rrsig.labels() as usize) < rrset.owner().iter_labels().count() - ); - Ok(Record::new( - rrset.owner().clone(), - rrset.class(), - rrset.ttl(), - rrsig, - )) + Ok(rrsig) } #[cfg(test)] @@ -370,7 +485,7 @@ mod tests { use crate::dnssec::sign::test_util; use crate::dnssec::sign::test_util::*; use crate::rdata::dnssec::Timestamp; - use crate::rdata::Dnskey; + use crate::rdata::{Dnskey, ZoneRecordData}; use crate::zonetree::StoredName; use super::*; @@ -627,7 +742,8 @@ mod tests { #[test] fn generate_rrsigs_without_keys_generates_no_rrsigs() { let apex = Name::from_str("example.").unwrap(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::<_, ZoneRecordData>::new(); records.insert(mk_a_rr("example.")).unwrap(); let no_keys: [&SigningKey; 0] = []; @@ -660,7 +776,8 @@ mod tests { // full zone, in this case just for an A record. This test // deliberately does not include a SOA record as the zone is partial. let apex = Name::from_str(zone_apex).unwrap(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::<_, ZoneRecordData>::new(); records.insert(mk_a_rr(record_owner)).unwrap(); // Prepare a zone signing key and a key signing key. @@ -701,7 +818,8 @@ mod tests { #[test] fn generate_rrsigs_ignores_records_outside_the_zone() { let apex = Name::from_str("example.").unwrap(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::<_, ZoneRecordData>::new(); records.extend([ mk_soa_rr("example.", "mname.", "rname."), mk_a_rr("in_zone.example."), @@ -742,7 +860,8 @@ mod tests { #[test] fn generate_rrsigs_ignores_glue_records() { let apex = Name::from_str("example.").unwrap(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::<_, ZoneRecordData>::new(); records.extend([ mk_soa_rr("example.", "mname.", "rname."), mk_ns_rr("example.", "early_sorting_glue."), @@ -1006,7 +1125,8 @@ mod tests { let apex = "example."; let apex_owner = Name::from_str(apex).unwrap(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::<_, ZoneRecordData>::new(); records.extend([ mk_soa_rr(apex, "some.mname.", "some.rname."), mk_ns_rr(apex, "ns.example."), @@ -1070,7 +1190,8 @@ mod tests { let dnskey = keys[0].dnskey().convert(); let apex = Name::from_str("example.").unwrap(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::<_, ZoneRecordData>::new(); records.extend([ // -- example. mk_soa_rr("example.", "some.mname.", "some.rname."), @@ -1190,7 +1311,7 @@ mod tests { dnskey: &Dnskey, ) -> Record where - R: From>, + R: From> + Send, { test_util::mk_rrsig_rr( name, @@ -1218,6 +1339,10 @@ mod tests { SecurityAlgorithm::ED25519 } + fn flags(&self) -> u16 { + todo!() + } + fn dnskey(&self) -> Dnskey> { let flags = 0; Dnskey::new(flags, 3, SecurityAlgorithm::ED25519, self.0.to_vec()) diff --git a/src/dnssec/sign/test_util/mod.rs b/src/dnssec/sign/test_util/mod.rs index b36c39d4c..17d0008ee 100644 --- a/src/dnssec/sign/test_util/mod.rs +++ b/src/dnssec/sign/test_util/mod.rs @@ -50,7 +50,7 @@ pub(crate) fn mk_record(owner: &str, data: D) -> Record { pub(crate) fn mk_a_rr(owner: &str) -> Record where - R: From, + R: From + Send, { mk_record(owner, A::from_str("1.2.3.4").unwrap().into()) } @@ -214,7 +214,7 @@ pub(crate) fn mk_rrsig_rr( signature: Bytes, ) -> Record where - R: From>, + R: From> + Send, { let signer_name = mk_name(signer_name); let expiration = Timestamp::from(expiration); @@ -243,7 +243,7 @@ pub(crate) fn mk_soa_rr( rname: &str, ) -> Record where - R: From>, + R: From> + Send, { let soa = Soa::new( mk_name(mname), @@ -254,7 +254,7 @@ where TEST_TTL, TEST_TTL, ); - mk_record(owner, soa.into()) + mk_record::(owner, soa.into()) } #[allow(clippy::type_complexity)] diff --git a/src/logging.rs b/src/logging.rs index 073cf1b64..a537cb6af 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -17,6 +17,8 @@ pub fn init_logging() { .with_env_filter(EnvFilter::from_default_env()) .with_thread_ids(true) .without_time() + // Useful sometimes: + // .with_span_events(tracing_subscriber::fmt::format::FmtSpan::NEW) .try_init() .ok(); } diff --git a/src/net/client/tsig.rs b/src/net/client/tsig.rs index 277690dce..2fbb5b70e 100644 --- a/src/net/client/tsig.rs +++ b/src/net/client/tsig.rs @@ -510,7 +510,7 @@ enum RequestState { /// the request prior to sending it, e.g. to assign a message ID or to add /// EDNS options, and signing **MUST** be the last modification made to the /// message prior to sending. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct RequestMessage where CR: Send + Sync, diff --git a/src/net/server/middleware/edns.rs b/src/net/server/middleware/edns.rs index cef77a066..75df8dc04 100644 --- a/src/net/server/middleware/edns.rs +++ b/src/net/server/middleware/edns.rs @@ -441,6 +441,7 @@ where ::Item, >; type Future = Ready; + fn call( &self, mut request: Request, diff --git a/src/net/server/middleware/xfr/service.rs b/src/net/server/middleware/xfr/service.rs index 8f330eaba..cb4f33f51 100644 --- a/src/net/server/middleware/xfr/service.rs +++ b/src/net/server/middleware/xfr/service.rs @@ -111,6 +111,11 @@ where xfr_data_provider: XDP, max_concurrency: usize, ) -> Self { + let max_concurrency = if max_concurrency > 0 { + max_concurrency + } else { + 1 + }; let zone_walking_semaphore = Arc::new(Semaphore::new(max_concurrency)); let batcher_semaphore = Arc::new(Semaphore::new(max_concurrency)); diff --git a/src/rdata/dnssec.rs b/src/rdata/dnssec.rs index 7091fd955..da94e9a54 100644 --- a/src/rdata/dnssec.rs +++ b/src/rdata/dnssec.rs @@ -4,6 +4,24 @@ //! //! [RFC 4034]: https://tools.ietf.org/html/rfc4034 +use core::cmp::Ordering; +use core::convert::TryInto; +use core::{cmp, fmt, hash, str}; + +#[cfg(feature = "std")] +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +#[cfg(feature = "std")] +use std::vec::Vec; + +use octseq::builder::{ + EmptyBuilder, FreezeBuilder, FromBuilder, OctetsBuilder, Truncate, +}; +use octseq::octets::{Octets, OctetsFrom, OctetsInto}; +use octseq::parse::Parser; +#[cfg(feature = "serde")] +use octseq::serde::{DeserializeOctets, SerializeOctets}; +use time::{Date, Month, PrimitiveDateTime, Time}; + use crate::base::cmp::CanonicalOrd; use crate::base::iana::{DigestAlgorithm, Rtype, SecurityAlgorithm}; use crate::base::name::{FlattenInto, ParsedName, ToName}; @@ -16,21 +34,6 @@ use crate::base::wire::{Compose, Composer, FormError, Parse, ParseError}; use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; use crate::base::Ttl; use crate::utils::{base16, base64}; -use core::cmp::Ordering; -use core::convert::TryInto; -use core::{cmp, fmt, hash, str}; -use octseq::builder::{ - EmptyBuilder, FreezeBuilder, FromBuilder, OctetsBuilder, Truncate, -}; -use octseq::octets::{Octets, OctetsFrom, OctetsInto}; -use octseq::parse::Parser; -#[cfg(feature = "serde")] -use octseq::serde::{DeserializeOctets, SerializeOctets}; -#[cfg(feature = "std")] -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -#[cfg(feature = "std")] -use std::vec::Vec; -use time::{Date, Month, PrimitiveDateTime, Time}; //------------ Dnskey -------------------------------------------------------- diff --git a/src/resolv/lookup/srv.rs b/src/resolv/lookup/srv.rs index f4a79f292..eec9248f1 100644 --- a/src/resolv/lookup/srv.rs +++ b/src/resolv/lookup/srv.rs @@ -144,6 +144,9 @@ impl FoundSrvs { if self.items.is_err() { let one = mem::replace(&mut self.items, Ok(Vec::new())).unwrap_err(); + + // False positive. -- XXX This whole thing should be re-written. + #[allow(clippy::panicking_unwrap)] self.items.as_mut().unwrap().push(one); } match self.items { diff --git a/src/zonetree/update.rs b/src/zonetree/update.rs index 700df5b07..74263be33 100644 --- a/src/zonetree/update.rs +++ b/src/zonetree/update.rs @@ -14,7 +14,7 @@ use tracing::trace; use crate::base::name::{FlattenInto, Label}; use crate::base::scan::ScannerError; -use crate::base::{Name, ParsedName, Record, Rtype, ToName}; +use crate::base::{Name, Record, Rtype, ToName}; use crate::rdata::ZoneRecordData; use crate::zonetree::{Rrset, SharedRrset}; @@ -76,8 +76,8 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// # /// # // Prepare some records to pass to ZoneUpdater /// # let serial = Serial::now(); -/// # let mname = ParsedName::from(Name::from_str("mname").unwrap()); -/// # let rname = ParsedName::from(Name::from_str("rname").unwrap()); +/// # let mname = ParsedName::from(Name::::from_str("mname").unwrap()); +/// # let rname = ParsedName::from(Name::::from_str("rname").unwrap()); /// # let ttl = Ttl::from_secs(0); /// # let new_soa_rec = Record::new( /// # ParsedName::from(Name::from_str("example.com").unwrap()), @@ -94,7 +94,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// # ZoneRecordData::A(A::new(Ipv4Addr::LOCALHOST)), /// # ); /// # -/// let mut updater = ZoneUpdater::>::new(zone.clone()).await.unwrap(); +/// let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); /// updater.apply(ZoneUpdate::DeleteAllRecords); /// updater.apply(ZoneUpdate::AddRecord(a_rec)); /// updater.apply(ZoneUpdate::Finished(new_soa_rec)); @@ -133,8 +133,8 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// # /// # // Prepare some records to pass to ZoneUpdater /// # let serial = Serial::now(); -/// # let mname = ParsedName::from(Name::from_str("mname").unwrap()); -/// # let rname = ParsedName::from(Name::from_str("rname").unwrap()); +/// # let mname = ParsedName::from(Name::::from_str("mname").unwrap()); +/// # let rname = ParsedName::from(Name::::from_str("rname").unwrap()); /// # let ttl = Ttl::from_secs(0); /// # let new_soa_rec = Record::new( /// # ParsedName::from(Name::from_str("example.com").unwrap()), @@ -159,7 +159,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// # ZoneRecordData::A(A::new(Ipv4Addr::LOCALHOST)), /// # ); /// # -/// let mut updater = ZoneUpdater::>::new(zone.clone()).await.unwrap(); +/// let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); /// updater.apply(ZoneUpdate::DeleteRecord(old_a_rec)); /// updater.apply(ZoneUpdate::AddRecord(new_aaaa_rec)); /// updater.apply(ZoneUpdate::Finished(new_soa_rec)); @@ -188,7 +188,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// let zone = builder.build(); /// /// // And a ZoneUpdater -/// let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); +/// let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); /// /// // And an XFR response interpreter /// let mut interpreter = XfrResponseInterpreter::new(); @@ -214,7 +214,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// ``` /// /// [`apply()`]: ZoneUpdater::apply() -pub struct ZoneUpdater> { +pub struct ZoneUpdater { /// The zone to be updated. zone: Zone, @@ -244,9 +244,11 @@ where /// Use [`apply`][Self::apply] to apply changes to the zone. pub fn new( zone: Zone, + create_diff: bool, ) -> Pin> + Send>> { Box::pin(async move { - let write = ReopenableZoneWriter::new(zone.clone()).await?; + let write = + ReopenableZoneWriter::new(zone.clone(), create_diff).await?; Ok(Self { zone, @@ -528,15 +530,22 @@ struct ReopenableZoneWriter { /// A write interface to the root node of a zone for a particular zone /// version. writable: Option>, + + /// Whether or not to create diffs on write. + create_diff: bool, } impl ReopenableZoneWriter { /// Creates a writer for the given [`Zone`]. - async fn new(zone: Zone) -> std::io::Result { + async fn new(zone: Zone, create_diff: bool) -> std::io::Result { let write = zone.write().await; - let writable = Some(write.open(true).await?); + let writable = Some(write.open(create_diff).await?); let write = Some(write); - Ok(Self { write, writable }) + Ok(Self { + write, + writable, + create_diff, + }) } /// Commits any pending changes to the [`Zone`] being written to. @@ -570,7 +579,7 @@ impl ReopenableZoneWriter { self.write .as_mut() .ok_or(Error::Finished)? - .open(true) + .open(self.create_diff) .await?, ); Ok(()) @@ -654,7 +663,7 @@ mod tests { let zone = mk_empty_zone("example.com"); - let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); + let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); let qname = Name::from_str("example.com").unwrap(); @@ -712,7 +721,7 @@ mod tests { let zone = mk_empty_zone("example.com"); - let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); + let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); let qname = Name::from_str("example.com").unwrap(); @@ -753,7 +762,7 @@ mod tests { let res = updater.apply(ZoneUpdate::AddRecord(soa_rec.clone())).await; assert!(matches!(res, Err(crate::zonetree::update::Error::Finished))); - let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); + let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); updater .apply(ZoneUpdate::AddRecord(soa_rec.clone())) @@ -802,7 +811,7 @@ mod tests { let zone = mk_empty_zone("example.com"); - let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); + let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); // Create an AXFR request to reply to. let req = mk_request("example.com", Rtype::AXFR).into_message(); @@ -913,7 +922,7 @@ mod tests { // serial number of 3," let zone = mk_empty_zone("JAIN.AD.JP."); - let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); + let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); // JAIN.AD.JP. IN SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. ( // 1 600 600 3600000 604800) let soa_1 = mk_rfc_1995_ixfr_example_soa(1); @@ -924,7 +933,7 @@ mod tests { // IN NS NS.JAIN.AD.JP. let ns_1 = Record::new( - ParsedName::from(Name::from_str("JAIN.AD.JP.").unwrap()), + ParsedName::from(Name::::from_str("JAIN.AD.JP.").unwrap()), Class::IN, Ttl::from_secs(0), Ns::new(ParsedName::from( @@ -939,7 +948,9 @@ mod tests { // NS.JAIN.AD.JP. IN A 133.69.136.1 let a_1 = Record::new( - ParsedName::from(Name::from_str("NS.JAIN.AD.JP.").unwrap()), + ParsedName::from( + Name::::from_str("NS.JAIN.AD.JP.").unwrap(), + ), Class::IN, Ttl::from_secs(0), A::new(Ipv4Addr::new(133, 69, 136, 1)).into(), @@ -951,7 +962,9 @@ mod tests { // NEZU.JAIN.AD.JP. IN A 133.69.136.5 let nezu = Record::new( - ParsedName::from(Name::from_str("NEZU.JAIN.AD.JP.").unwrap()), + ParsedName::from( + Name::::from_str("NEZU.JAIN.AD.JP.").unwrap(), + ), Class::IN, Ttl::from_secs(0), A::new(Ipv4Addr::new(133, 69, 136, 5)).into(), @@ -976,7 +989,9 @@ mod tests { .await .unwrap(); let a_2 = Record::new( - ParsedName::from(Name::from_str("JAIN-BB.JAIN.AD.JP.").unwrap()), + ParsedName::from( + Name::::from_str("JAIN-BB.JAIN.AD.JP.").unwrap(), + ), Class::IN, Ttl::from_secs(0), A::new(Ipv4Addr::new(133, 69, 136, 4)).into(), @@ -1011,7 +1026,9 @@ mod tests { .await .unwrap(); let a_4 = Record::new( - ParsedName::from(Name::from_str("JAIN-BB.JAIN.AD.JP.").unwrap()), + ParsedName::from( + Name::::from_str("JAIN-BB.JAIN.AD.JP.").unwrap(), + ), Class::IN, Ttl::from_secs(0), A::new(Ipv4Addr::new(133, 69, 136, 3)).into(), @@ -1208,7 +1225,7 @@ mod tests { let zone = mk_empty_zone("example.com"); - let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); + let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); // Create an AXFR request to reply to. let req = mk_request("example.com", Rtype::AXFR).into_message();