From a813b6553544ca757ef310d6b5f09a70382b7e0b Mon Sep 17 00:00:00 2001 From: Adam Wick Date: Mon, 14 Oct 2024 17:33:24 -0700 Subject: [PATCH] well, tests pass now --- .cargo/config.toml | 2 + .gitignore | 8 +- Cargo.lock | 2634 +++++++++++++++++++++++++ Cargo.toml | 54 + specifications/rfc4253.txt | 1795 +++++++++++++++++ specifications/rfc6668.txt | 283 +++ specifications/rfc8308.txt | 787 ++++++++ specifications/rfc8332.txt | 507 +++++ specifications/rfc8709.txt | 317 +++ specifications/rfc8758.txt | 196 ++ specifications/rfc9142.txt | 1028 ++++++++++ specifications/rfc9519.txt | 272 +++ src/bin/hush.rs | 39 + src/bin/hushd.rs | 4 + src/client.rs | 164 ++ src/config.rs | 298 +++ src/config/command_line.rs | 205 ++ src/config/config_file.rs | 423 ++++ src/config/console.rs | 48 + src/config/error.rs | 22 + src/config/logging.rs | 95 + src/config/resolver.rs | 304 +++ src/crypto.rs | 272 +++ src/crypto/known_algorithms.rs | 24 + src/crypto/rsa.rs | 251 +++ src/encodings.rs | 1 + src/encodings/ssh.rs | 10 + src/encodings/ssh/buffer.rs | 195 ++ src/encodings/ssh/private_key.rs | 844 ++++++++ src/encodings/ssh/private_key_file.rs | 816 ++++++++ src/encodings/ssh/public_key.rs | 448 +++++ src/encodings/ssh/public_key_file.rs | 204 ++ src/lib.rs | 9 + src/network.rs | 1 + src/network/host.rs | 211 ++ src/operational_error.rs | 47 + src/ssh.rs | 8 + src/ssh/channel.rs | 428 ++++ src/ssh/message_ids.rs | 173 ++ src/ssh/packets.rs | 3 + src/ssh/packets/key_exchange.rs | 332 ++++ src/ssh/preamble.rs | 392 ++++ tests/all_keys.toml | 113 ++ tests/broken_keys/bad_info | 53 + tests/broken_keys/mismatched | 9 + tests/ssh_keys/ecdsa1 | 9 + tests/ssh_keys/ecdsa1.pub | 1 + tests/ssh_keys/ecdsa2 | 9 + tests/ssh_keys/ecdsa2.pub | 1 + tests/ssh_keys/ecdsa384a | 10 + tests/ssh_keys/ecdsa384a.pub | 1 + tests/ssh_keys/ecdsa384b | 10 + tests/ssh_keys/ecdsa384b.pub | 1 + tests/ssh_keys/ecdsa521a | 12 + tests/ssh_keys/ecdsa521a.pub | 1 + tests/ssh_keys/ecdsa521b | 12 + tests/ssh_keys/ecdsa521b.pub | 1 + tests/ssh_keys/ed25519_pw_hush1 | 8 + tests/ssh_keys/ed25519_pw_hush1.pub | 1 + tests/ssh_keys/ed25519_pw_hush2 | 8 + tests/ssh_keys/ed25519_pw_hush2.pub | 1 + tests/ssh_keys/ed25519a | 7 + tests/ssh_keys/ed25519a.pub | 1 + tests/ssh_keys/ed25519b | 7 + tests/ssh_keys/ed25519b.pub | 1 + tests/ssh_keys/rsa15360a | 170 ++ tests/ssh_keys/rsa15360a.pub | 1 + tests/ssh_keys/rsa15360b | 170 ++ tests/ssh_keys/rsa15360b.pub | 1 + tests/ssh_keys/rsa4096a | 49 + tests/ssh_keys/rsa4096a.pub | 1 + tests/ssh_keys/rsa4096b | 49 + tests/ssh_keys/rsa4096b.pub | 1 + tests/ssh_keys/rsa7680a | 88 + tests/ssh_keys/rsa7680a.pub | 1 + tests/ssh_keys/rsa7680b | 88 + tests/ssh_keys/rsa7680b.pub | 1 + tests/ssh_keys/rsa8192a | 93 + tests/ssh_keys/rsa8192a.pub | 1 + tests/ssh_keys/rsa8192b | 93 + tests/ssh_keys/rsa8192b.pub | 1 + 81 files changed, 15233 insertions(+), 6 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 specifications/rfc4253.txt create mode 100644 specifications/rfc6668.txt create mode 100644 specifications/rfc8308.txt create mode 100644 specifications/rfc8332.txt create mode 100644 specifications/rfc8709.txt create mode 100644 specifications/rfc8758.txt create mode 100644 specifications/rfc9142.txt create mode 100644 specifications/rfc9519.txt create mode 100644 src/bin/hush.rs create mode 100644 src/bin/hushd.rs create mode 100644 src/client.rs create mode 100644 src/config.rs create mode 100644 src/config/command_line.rs create mode 100644 src/config/config_file.rs create mode 100644 src/config/console.rs create mode 100644 src/config/error.rs create mode 100644 src/config/logging.rs create mode 100644 src/config/resolver.rs create mode 100644 src/crypto.rs create mode 100644 src/crypto/known_algorithms.rs create mode 100644 src/crypto/rsa.rs create mode 100644 src/encodings.rs create mode 100644 src/encodings/ssh.rs create mode 100644 src/encodings/ssh/buffer.rs create mode 100644 src/encodings/ssh/private_key.rs create mode 100644 src/encodings/ssh/private_key_file.rs create mode 100644 src/encodings/ssh/public_key.rs create mode 100644 src/encodings/ssh/public_key_file.rs create mode 100644 src/lib.rs create mode 100644 src/network.rs create mode 100644 src/network/host.rs create mode 100644 src/operational_error.rs create mode 100644 src/ssh.rs create mode 100644 src/ssh/channel.rs create mode 100644 src/ssh/message_ids.rs create mode 100644 src/ssh/packets.rs create mode 100644 src/ssh/packets/key_exchange.rs create mode 100644 src/ssh/preamble.rs create mode 100644 tests/all_keys.toml create mode 100644 tests/broken_keys/bad_info create mode 100644 tests/broken_keys/mismatched create mode 100644 tests/ssh_keys/ecdsa1 create mode 100644 tests/ssh_keys/ecdsa1.pub create mode 100644 tests/ssh_keys/ecdsa2 create mode 100644 tests/ssh_keys/ecdsa2.pub create mode 100644 tests/ssh_keys/ecdsa384a create mode 100644 tests/ssh_keys/ecdsa384a.pub create mode 100644 tests/ssh_keys/ecdsa384b create mode 100644 tests/ssh_keys/ecdsa384b.pub create mode 100644 tests/ssh_keys/ecdsa521a create mode 100644 tests/ssh_keys/ecdsa521a.pub create mode 100644 tests/ssh_keys/ecdsa521b create mode 100644 tests/ssh_keys/ecdsa521b.pub create mode 100644 tests/ssh_keys/ed25519_pw_hush1 create mode 100644 tests/ssh_keys/ed25519_pw_hush1.pub create mode 100644 tests/ssh_keys/ed25519_pw_hush2 create mode 100644 tests/ssh_keys/ed25519_pw_hush2.pub create mode 100644 tests/ssh_keys/ed25519a create mode 100644 tests/ssh_keys/ed25519a.pub create mode 100644 tests/ssh_keys/ed25519b create mode 100644 tests/ssh_keys/ed25519b.pub create mode 100644 tests/ssh_keys/rsa15360a create mode 100644 tests/ssh_keys/rsa15360a.pub create mode 100644 tests/ssh_keys/rsa15360b create mode 100644 tests/ssh_keys/rsa15360b.pub create mode 100644 tests/ssh_keys/rsa4096a create mode 100644 tests/ssh_keys/rsa4096a.pub create mode 100644 tests/ssh_keys/rsa4096b create mode 100644 tests/ssh_keys/rsa4096b.pub create mode 100644 tests/ssh_keys/rsa7680a create mode 100644 tests/ssh_keys/rsa7680a.pub create mode 100644 tests/ssh_keys/rsa7680b create mode 100644 tests/ssh_keys/rsa7680b.pub create mode 100644 tests/ssh_keys/rsa8192a create mode 100644 tests/ssh_keys/rsa8192a.pub create mode 100644 tests/ssh_keys/rsa8192b create mode 100644 tests/ssh_keys/rsa8192b.pub diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..16f1e73 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["--cfg", "tokio_unstable"] \ No newline at end of file diff --git a/.gitignore b/.gitignore index d01bd1a..a047120 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,5 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb -# RustRover -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +tarpaulin-report.html +proptest-regressions/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..76308b2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2634 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "console-api" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257c22cd7e487dd4a13d413beabc512c5052f0bc048db0da6a84c3d8a6142fd" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c4cc54bae66f7d9188996404abdf7fdfa23034ef8e43478c8810828abad758" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "prost", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-stack" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe413319145d1063f080f27556fd30b1d70b01e2ba10c2a6e40d4be982ffc5d1" +dependencies = [ + "anyhow", + "rustc_version", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.6.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64 0.21.7", + "byteorder", + "flate2", + "nom", + "num-traits", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hexdump" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf31ab66ed8145a1c7427bd8e9b42a6131bd74ccf444f69b9e620c2e73ded832" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hickory-proto" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07698b8420e2f0d6447a436ba999ec85d8fbf2a398bbd737b82cac4a2e96e512" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.4.0", + "ipnet", + "once_cell", + "rand", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28757f23aa75c98f254cf0405e6d8c25b831b32921b050a66692427679b1f243" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "hostname-validator" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hushd" +version = "0.1.0" +dependencies = [ + "aes", + "base64 0.22.1", + "bcrypt-pbkdf", + "bytes", + "cipher", + "clap", + "console-subscriber", + "ctr", + "ed25519-dalek", + "elliptic-curve", + "error-stack", + "futures", + "generic-array", + "hexdump", + "hickory-proto", + "hickory-resolver", + "hostname-validator", + "itertools 0.13.0", + "num-bigint-dig", + "num-integer", + "num-traits", + "num_enum", + "p256", + "p384", + "p521", + "proptest", + "rand", + "rand_chacha", + "sec1", + "serde", + "tempfile", + "thiserror", + "tokio", + "toml", + "tracing", + "tracing-core", + "tracing-subscriber", + "whoami", + "xdg", + "zeroize", +] + +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "arbitrary", + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "hex-literal 0.4.1", + "primeorder", + "serdect", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "hex-literal 0.3.4", + "primeorder", + "serdect", + "sha2", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "hex-literal 0.4.1", + "primeorder", + "rand_core", + "serdect", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", + "serdect", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.6.0", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.8.5", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tonic" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.21.7", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3efa3ce --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "hushd" +version = "0.1.0" +edition = "2021" +authors = ["awick"] + +[lib] +name = "hush" +path = "src/lib.rs" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } + +[dependencies] +aes = { version = "0.8.4", features = ["zeroize"] } +base64 = "0.22.1" +bcrypt-pbkdf = "0.10.0" +bytes = "1.6.0" +cipher = { version = "0.4.4", features = ["alloc", "block-padding", "rand_core", "std", "zeroize"] } +clap = { version = "4.5.7", features = ["derive"] } +console-subscriber = "0.3.0" +ctr = "0.9.2" +ed25519-dalek = "2.1.1" +elliptic-curve = { version = "0.13.8", features = ["alloc", "digest", "ecdh", "pem", "pkcs8", "sec1", "serde", "std", "hash2curve", "voprf"] } +error-stack = "0.5.0" +futures = "0.3.31" +generic-array = "0.14.7" +hexdump = "0.1.2" +hickory-proto = "0.24.1" +hickory-resolver = "0.24.1" +hostname-validator = "1.1.1" +itertools = "0.13.0" +num-bigint-dig = { version = "0.8.4", features = ["arbitrary", "i128", "zeroize", "prime", "rand"] } +num-integer = { version = "0.1.46", features = ["i128"] } +num-traits = { version = "0.2.19", features = ["i128"] } +num_enum = "0.7.2" +p256 = { version = "0.13.2", features = ["ecdh", "ecdsa-core", "hash2curve", "serde", "test-vectors"] } +p384 = { version = "0.13.0", features = ["ecdh", "ecdsa-core", "hash2curve", "serde", "test-vectors"] } +p521 = { version = "0.13.3", features = ["ecdh", "ecdsa-core", "hash2curve", "serde", "test-vectors"] } +proptest = "1.5.0" +rand = "0.8.5" +rand_chacha = "0.3.1" +sec1 = "0.7.3" +serde = { version = "1.0.203", features = ["derive"] } +tempfile = "3.12.0" +thiserror = "1.0.61" +tokio = { version = "1.38.0", features = ["full", "tracing"] } +toml = "0.8.14" +tracing = "0.1.40" +tracing-core = "0.1.32" +tracing-subscriber = { version = "0.3.18", features = ["env-filter", "tracing", "json"] } +whoami = { version = "1.5.2", default-features = false } +xdg = "2.5.2" +zeroize = "1.8.1" diff --git a/specifications/rfc4253.txt b/specifications/rfc4253.txt new file mode 100644 index 0000000..20e296e --- /dev/null +++ b/specifications/rfc4253.txt @@ -0,0 +1,1795 @@ + + + + + + +Network Working Group T. Ylonen +Request for Comments: 4253 SSH Communications Security Corp +Category: Standards Track C. Lonvick, Ed. + Cisco Systems, Inc. + January 2006 + + + The Secure Shell (SSH) Transport Layer Protocol + +Status of This Memo + + This document specifies an Internet standards track protocol for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "Internet + Official Protocol Standards" (STD 1) for the standardization state + and status of this protocol. Distribution of this memo is unlimited. + +Copyright Notice + + Copyright (C) The Internet Society (2006). + +Abstract + + The Secure Shell (SSH) is a protocol for secure remote login and + other secure network services over an insecure network. + + This document describes the SSH transport layer protocol, which + typically runs on top of TCP/IP. The protocol can be used as a basis + for a number of secure network services. It provides strong + encryption, server authentication, and integrity protection. It may + also provide compression. + + Key exchange method, public key algorithm, symmetric encryption + algorithm, message authentication algorithm, and hash algorithm are + all negotiated. + + This document also describes the Diffie-Hellman key exchange method + and the minimal set of algorithms that are needed to implement the + SSH transport layer protocol. + + + + + + + + + + + + +Ylonen & Lonvick Standards Track [Page 1] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + +Table of Contents + + 1. Introduction ....................................................3 + 2. Contributors ....................................................3 + 3. Conventions Used in This Document ...............................3 + 4. Connection Setup ................................................4 + 4.1. Use over TCP/IP ............................................4 + 4.2. Protocol Version Exchange ..................................4 + 5. Compatibility With Old SSH Versions .............................5 + 5.1. Old Client, New Server .....................................6 + 5.2. New Client, Old Server .....................................6 + 5.3. Packet Size and Overhead ...................................6 + 6. Binary Packet Protocol ..........................................7 + 6.1. Maximum Packet Length ......................................8 + 6.2. Compression ................................................8 + 6.3. Encryption .................................................9 + 6.4. Data Integrity ............................................12 + 6.5. Key Exchange Methods ......................................13 + 6.6. Public Key Algorithms .....................................13 + 7. Key Exchange ...................................................15 + 7.1. Algorithm Negotiation .....................................17 + 7.2. Output from Key Exchange ..................................20 + 7.3. Taking Keys Into Use ......................................21 + 8. Diffie-Hellman Key Exchange ....................................21 + 8.1. diffie-hellman-group1-sha1 ................................23 + 8.2. diffie-hellman-group14-sha1 ...............................23 + 9. Key Re-Exchange ................................................23 + 10. Service Request ...............................................24 + 11. Additional Messages ...........................................25 + 11.1. Disconnection Message ....................................25 + 11.2. Ignored Data Message .....................................26 + 11.3. Debug Message ............................................26 + 11.4. Reserved Messages ........................................27 + 12. Summary of Message Numbers ....................................27 + 13. IANA Considerations ...........................................27 + 14. Security Considerations .......................................28 + 15. References ....................................................29 + 15.1. Normative References .....................................29 + 15.2. Informative References ...................................30 + Authors' Addresses ................................................31 + Trademark Notice ..................................................31 + + + + + + + + + + +Ylonen & Lonvick Standards Track [Page 2] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + +1. Introduction + + The SSH transport layer is a secure, low level transport protocol. + It provides strong encryption, cryptographic host authentication, and + integrity protection. + + Authentication in this protocol level is host-based; this protocol + does not perform user authentication. A higher level protocol for + user authentication can be designed on top of this protocol. + + The protocol has been designed to be simple and flexible to allow + parameter negotiation, and to minimize the number of round-trips. + The key exchange method, public key algorithm, symmetric encryption + algorithm, message authentication algorithm, and hash algorithm are + all negotiated. It is expected that in most environments, only 2 + round-trips will be needed for full key exchange, server + authentication, service request, and acceptance notification of + service request. The worst case is 3 round-trips. + +2. Contributors + + The major original contributors of this set of documents have been: + Tatu Ylonen, Tero Kivinen, Timo J. Rinne, Sami Lehtinen (all of SSH + Communications Security Corp), and Markku-Juhani O. Saarinen + (University of Jyvaskyla). Darren Moffat was the original editor of + this set of documents and also made very substantial contributions. + + Many people contributed to the development of this document over the + years. People who should be acknowledged include Mats Andersson, Ben + Harris, Bill Sommerfeld, Brent McClure, Niels Moller, Damien Miller, + Derek Fawcus, Frank Cusack, Heikki Nousiainen, Jakob Schlyter, Jeff + Van Dyke, Jeffrey Altman, Jeffrey Hutzelman, Jon Bright, Joseph + Galbraith, Ken Hornstein, Markus Friedl, Martin Forssen, Nicolas + Williams, Niels Provos, Perry Metzger, Peter Gutmann, Simon + Josefsson, Simon Tatham, Wei Dai, Denis Bider, der Mouse, and + Tadayoshi Kohno. Listing their names here does not mean that they + endorse this document, but that they have contributed to it. + +3. Conventions Used in This Document + + All documents related to the SSH protocols shall use the keywords + "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", + "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" to describe + requirements. These keywords are to be interpreted as described in + [RFC2119]. + + + + + + +Ylonen & Lonvick Standards Track [Page 3] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + The keywords "PRIVATE USE", "HIERARCHICAL ALLOCATION", "FIRST COME + FIRST SERVED", "EXPERT REVIEW", "SPECIFICATION REQUIRED", "IESG + APPROVAL", "IETF CONSENSUS", and "STANDARDS ACTION" that appear in + this document when used to describe namespace allocation are to be + interpreted as described in [RFC2434]. + + Protocol fields and possible values to fill them are defined in this + set of documents. Protocol fields will be defined in the message + definitions. As an example, SSH_MSG_CHANNEL_DATA is defined as + follows. + + byte SSH_MSG_CHANNEL_DATA + uint32 recipient channel + string data + + Throughout these documents, when the fields are referenced, they will + appear within single quotes. When values to fill those fields are + referenced, they will appear within double quotes. Using the above + example, possible values for 'data' are "foo" and "bar". + +4. Connection Setup + + SSH works over any 8-bit clean, binary-transparent transport. The + underlying transport SHOULD protect against transmission errors, as + such errors cause the SSH connection to terminate. + + The client initiates the connection. + +4.1. Use over TCP/IP + + When used over TCP/IP, the server normally listens for connections on + port 22. This port number has been registered with the IANA, and has + been officially assigned for SSH. + +4.2. Protocol Version Exchange + + When the connection has been established, both sides MUST send an + identification string. This identification string MUST be + + SSH-protoversion-softwareversion SP comments CR LF + + Since the protocol being defined in this set of documents is version + 2.0, the 'protoversion' MUST be "2.0". The 'comments' string is + OPTIONAL. If the 'comments' string is included, a 'space' character + (denoted above as SP, ASCII 32) MUST separate the 'softwareversion' + and 'comments' strings. The identification MUST be terminated by a + single Carriage Return (CR) and a single Line Feed (LF) character + (ASCII 13 and 10, respectively). Implementers who wish to maintain + + + +Ylonen & Lonvick Standards Track [Page 4] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + compatibility with older, undocumented versions of this protocol may + want to process the identification string without expecting the + presence of the carriage return character for reasons described in + Section 5 of this document. The null character MUST NOT be sent. + The maximum length of the string is 255 characters, including the + Carriage Return and Line Feed. + + The part of the identification string preceding the Carriage Return + and Line Feed is used in the Diffie-Hellman key exchange (see Section + 8). + + The server MAY send other lines of data before sending the version + string. Each line SHOULD be terminated by a Carriage Return and Line + Feed. Such lines MUST NOT begin with "SSH-", and SHOULD be encoded + in ISO-10646 UTF-8 [RFC3629] (language is not specified). Clients + MUST be able to process such lines. Such lines MAY be silently + ignored, or MAY be displayed to the client user. If they are + displayed, control character filtering, as discussed in [SSH-ARCH], + SHOULD be used. The primary use of this feature is to allow TCP- + wrappers to display an error message before disconnecting. + + Both the 'protoversion' and 'softwareversion' strings MUST consist of + printable US-ASCII characters, with the exception of whitespace + characters and the minus sign (-). The 'softwareversion' string is + primarily used to trigger compatibility extensions and to indicate + the capabilities of an implementation. The 'comments' string SHOULD + contain additional information that might be useful in solving user + problems. As such, an example of a valid identification string is + + SSH-2.0-billsSSH_3.6.3q3 + + This identification string does not contain the optional 'comments' + string and is thus terminated by a CR and LF immediately after the + 'softwareversion' string. + + Key exchange will begin immediately after sending this identifier. + All packets following the identification string SHALL use the binary + packet protocol, which is described in Section 6. + +5. Compatibility With Old SSH Versions + + As stated earlier, the 'protoversion' specified for this protocol is + "2.0". Earlier versions of this protocol have not been formally + documented, but it is widely known that they use 'protoversion' of + "1.x" (e.g., "1.5" or "1.3"). At the time of this writing, many + implementations of SSH are utilizing protocol version 2.0, but it is + known that there are still devices using the previous versions. + During the transition period, it is important to be able to work in a + + + +Ylonen & Lonvick Standards Track [Page 5] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + way that is compatible with the installed SSH clients and servers + that use the older version of the protocol. Information in this + section is only relevant for implementations supporting compatibility + with SSH versions 1.x. For those interested, the only known + documentation of the 1.x protocol is contained in README files that + are shipped along with the source code [ssh-1.2.30]. + +5.1. Old Client, New Server + + Server implementations MAY support a configurable compatibility flag + that enables compatibility with old versions. When this flag is on, + the server SHOULD identify its 'protoversion' as "1.99". Clients + using protocol 2.0 MUST be able to identify this as identical to + "2.0". In this mode, the server SHOULD NOT send the Carriage Return + character (ASCII 13) after the identification string. + + In the compatibility mode, the server SHOULD NOT send any further + data after sending its identification string until it has received an + identification string from the client. The server can then determine + whether the client is using an old protocol, and can revert to the + old protocol if required. In the compatibility mode, the server MUST + NOT send additional data before the identification string. + + When compatibility with old clients is not needed, the server MAY + send its initial key exchange data immediately after the + identification string. + +5.2. New Client, Old Server + + Since the new client MAY immediately send additional data after its + identification string (before receiving the server's identification + string), the old protocol may already be corrupt when the client + learns that the server is old. When this happens, the client SHOULD + close the connection to the server, and reconnect using the old + protocol. + +5.3. Packet Size and Overhead + + Some readers will worry about the increase in packet size due to new + headers, padding, and the Message Authentication Code (MAC). The + minimum packet size is in the order of 28 bytes (depending on + negotiated algorithms). The increase is negligible for large + packets, but very significant for one-byte packets (telnet-type + sessions). There are, however, several factors that make this a + non-issue in almost all cases: + + o The minimum size of a TCP/IP header is 32 bytes. Thus, the + increase is actually from 33 to 51 bytes (roughly). + + + +Ylonen & Lonvick Standards Track [Page 6] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + o The minimum size of the data field of an Ethernet packet is 46 + bytes [RFC0894]. Thus, the increase is no more than 5 bytes. + When Ethernet headers are considered, the increase is less than 10 + percent. + + o The total fraction of telnet-type data in the Internet is + negligible, even with increased packet sizes. + + The only environment where the packet size increase is likely to have + a significant effect is PPP [RFC1661] over slow modem lines (PPP + compresses the TCP/IP headers, emphasizing the increase in packet + size). However, with modern modems, the time needed to transfer is + in the order of 2 milliseconds, which is a lot faster than people can + type. + + There are also issues related to the maximum packet size. To + minimize delays in screen updates, one does not want excessively + large packets for interactive sessions. The maximum packet size is + negotiated separately for each channel. + +6. Binary Packet Protocol + + Each packet is in the following format: + + uint32 packet_length + byte padding_length + byte[n1] payload; n1 = packet_length - padding_length - 1 + byte[n2] random padding; n2 = padding_length + byte[m] mac (Message Authentication Code - MAC); m = mac_length + + packet_length + The length of the packet in bytes, not including 'mac' or the + 'packet_length' field itself. + + padding_length + Length of 'random padding' (bytes). + + payload + The useful contents of the packet. If compression has been + negotiated, this field is compressed. Initially, compression + MUST be "none". + + random padding + Arbitrary-length padding, such that the total length of + (packet_length || padding_length || payload || random padding) + is a multiple of the cipher block size or 8, whichever is + + + + + +Ylonen & Lonvick Standards Track [Page 7] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + larger. There MUST be at least four bytes of padding. The + padding SHOULD consist of random bytes. The maximum amount of + padding is 255 bytes. + + mac + Message Authentication Code. If message authentication has + been negotiated, this field contains the MAC bytes. Initially, + the MAC algorithm MUST be "none". + + Note that the length of the concatenation of 'packet_length', + 'padding_length', 'payload', and 'random padding' MUST be a multiple + of the cipher block size or 8, whichever is larger. This constraint + MUST be enforced, even when using stream ciphers. Note that the + 'packet_length' field is also encrypted, and processing it requires + special care when sending or receiving packets. Also note that the + insertion of variable amounts of 'random padding' may help thwart + traffic analysis. + + The minimum size of a packet is 16 (or the cipher block size, + whichever is larger) bytes (plus 'mac'). Implementations SHOULD + decrypt the length after receiving the first 8 (or cipher block size, + whichever is larger) bytes of a packet. + +6.1. Maximum Packet Length + + All implementations MUST be able to process packets with an + uncompressed payload length of 32768 bytes or less and a total packet + size of 35000 bytes or less (including 'packet_length', + 'padding_length', 'payload', 'random padding', and 'mac'). The + maximum of 35000 bytes is an arbitrarily chosen value that is larger + than the uncompressed length noted above. Implementations SHOULD + support longer packets, where they might be needed. For example, if + an implementation wants to send a very large number of certificates, + the larger packets MAY be sent if the identification string indicates + that the other party is able to process them. However, + implementations SHOULD check that the packet length is reasonable in + order for the implementation to avoid denial of service and/or buffer + overflow attacks. + +6.2. Compression + + If compression has been negotiated, the 'payload' field (and only it) + will be compressed using the negotiated algorithm. The + 'packet_length' field and 'mac' will be computed from the compressed + payload. Encryption will be done after compression. + + + + + + +Ylonen & Lonvick Standards Track [Page 8] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + Compression MAY be stateful, depending on the method. Compression + MUST be independent for each direction, and implementations MUST + allow independent choosing of the algorithm for each direction. In + practice however, it is RECOMMENDED that the compression method be + the same in both directions. + + The following compression methods are currently defined: + + none REQUIRED no compression + zlib OPTIONAL ZLIB (LZ77) compression + + The "zlib" compression is described in [RFC1950] and in [RFC1951]. + The compression context is initialized after each key exchange, and + is passed from one packet to the next, with only a partial flush + being performed at the end of each packet. A partial flush means + that the current compressed block is ended and all data will be + output. If the current block is not a stored block, one or more + empty blocks are added after the current block to ensure that there + are at least 8 bits, counting from the start of the end-of-block code + of the current block to the end of the packet payload. + + Additional methods may be defined as specified in [SSH-ARCH] and + [SSH-NUMBERS]. + +6.3. Encryption + + An encryption algorithm and a key will be negotiated during the key + exchange. When encryption is in effect, the packet length, padding + length, payload, and padding fields of each packet MUST be encrypted + with the given algorithm. + + The encrypted data in all packets sent in one direction SHOULD be + considered a single data stream. For example, initialization vectors + SHOULD be passed from the end of one packet to the beginning of the + next packet. All ciphers SHOULD use keys with an effective key + length of 128 bits or more. + + The ciphers in each direction MUST run independently of each other. + Implementations MUST allow the algorithm for each direction to be + independently selected, if multiple algorithms are allowed by local + policy. In practice however, it is RECOMMENDED that the same + algorithm be used in both directions. + + + + + + + + + +Ylonen & Lonvick Standards Track [Page 9] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + The following ciphers are currently defined: + + 3des-cbc REQUIRED three-key 3DES in CBC mode + blowfish-cbc OPTIONAL Blowfish in CBC mode + twofish256-cbc OPTIONAL Twofish in CBC mode, + with a 256-bit key + twofish-cbc OPTIONAL alias for "twofish256-cbc" + (this is being retained + for historical reasons) + twofish192-cbc OPTIONAL Twofish with a 192-bit key + twofish128-cbc OPTIONAL Twofish with a 128-bit key + aes256-cbc OPTIONAL AES in CBC mode, + with a 256-bit key + aes192-cbc OPTIONAL AES with a 192-bit key + aes128-cbc RECOMMENDED AES with a 128-bit key + serpent256-cbc OPTIONAL Serpent in CBC mode, with + a 256-bit key + serpent192-cbc OPTIONAL Serpent with a 192-bit key + serpent128-cbc OPTIONAL Serpent with a 128-bit key + arcfour OPTIONAL the ARCFOUR stream cipher + with a 128-bit key + idea-cbc OPTIONAL IDEA in CBC mode + cast128-cbc OPTIONAL CAST-128 in CBC mode + none OPTIONAL no encryption; NOT RECOMMENDED + + The "3des-cbc" cipher is three-key triple-DES (encrypt-decrypt- + encrypt), where the first 8 bytes of the key are used for the first + encryption, the next 8 bytes for the decryption, and the following 8 + bytes for the final encryption. This requires 24 bytes of key data + (of which 168 bits are actually used). To implement CBC mode, outer + chaining MUST be used (i.e., there is only one initialization + vector). This is a block cipher with 8-byte blocks. This algorithm + is defined in [FIPS-46-3]. Note that since this algorithm only has + an effective key length of 112 bits ([SCHNEIER]), it does not meet + the specifications that SSH encryption algorithms should use keys of + 128 bits or more. However, this algorithm is still REQUIRED for + historical reasons; essentially, all known implementations at the + time of this writing support this algorithm, and it is commonly used + because it is the fundamental interoperable algorithm. At some + future time, it is expected that another algorithm, one with better + strength, will become so prevalent and ubiquitous that the use of + "3des-cbc" will be deprecated by another STANDARDS ACTION. + + The "blowfish-cbc" cipher is Blowfish in CBC mode, with 128-bit keys + [SCHNEIER]. This is a block cipher with 8-byte blocks. + + + + + + +Ylonen & Lonvick Standards Track [Page 10] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + The "twofish-cbc" or "twofish256-cbc" cipher is Twofish in CBC mode, + with 256-bit keys as described [TWOFISH]. This is a block cipher + with 16-byte blocks. + + The "twofish192-cbc" cipher is the same as above, but with a 192-bit + key. + + The "twofish128-cbc" cipher is the same as above, but with a 128-bit + key. + + The "aes256-cbc" cipher is AES (Advanced Encryption Standard) + [FIPS-197], in CBC mode. This version uses a 256-bit key. + + The "aes192-cbc" cipher is the same as above, but with a 192-bit key. + + The "aes128-cbc" cipher is the same as above, but with a 128-bit key. + + The "serpent256-cbc" cipher in CBC mode, with a 256-bit key as + described in the Serpent AES submission. + + The "serpent192-cbc" cipher is the same as above, but with a 192-bit + key. + + The "serpent128-cbc" cipher is the same as above, but with a 128-bit + key. + + The "arcfour" cipher is the Arcfour stream cipher with 128-bit keys. + The Arcfour cipher is believed to be compatible with the RC4 cipher + [SCHNEIER]. Arcfour (and RC4) has problems with weak keys, and + should be used with caution. + + The "idea-cbc" cipher is the IDEA cipher in CBC mode [SCHNEIER]. + + The "cast128-cbc" cipher is the CAST-128 cipher in CBC mode with a + 128-bit key [RFC2144]. + + The "none" algorithm specifies that no encryption is to be done. + Note that this method provides no confidentiality protection, and it + is NOT RECOMMENDED. Some functionality (e.g., password + authentication) may be disabled for security reasons if this cipher + is chosen. + + Additional methods may be defined as specified in [SSH-ARCH] and in + [SSH-NUMBERS]. + + + + + + + +Ylonen & Lonvick Standards Track [Page 11] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + +6.4. Data Integrity + + Data integrity is protected by including with each packet a MAC that + is computed from a shared secret, packet sequence number, and the + contents of the packet. + + The message authentication algorithm and key are negotiated during + key exchange. Initially, no MAC will be in effect, and its length + MUST be zero. After key exchange, the 'mac' for the selected MAC + algorithm will be computed before encryption from the concatenation + of packet data: + + mac = MAC(key, sequence_number || unencrypted_packet) + + where unencrypted_packet is the entire packet without 'mac' (the + length fields, 'payload' and 'random padding'), and sequence_number + is an implicit packet sequence number represented as uint32. The + sequence_number is initialized to zero for the first packet, and is + incremented after every packet (regardless of whether encryption or + MAC is in use). It is never reset, even if keys/algorithms are + renegotiated later. It wraps around to zero after every 2^32 + packets. The packet sequence_number itself is not included in the + packet sent over the wire. + + The MAC algorithms for each direction MUST run independently, and + implementations MUST allow choosing the algorithm independently for + both directions. In practice however, it is RECOMMENDED that the + same algorithm be used in both directions. + + The value of 'mac' resulting from the MAC algorithm MUST be + transmitted without encryption as the last part of the packet. The + number of 'mac' bytes depends on the algorithm chosen. + + The following MAC algorithms are currently defined: + + hmac-sha1 REQUIRED HMAC-SHA1 (digest length = key + length = 20) + hmac-sha1-96 RECOMMENDED first 96 bits of HMAC-SHA1 (digest + length = 12, key length = 20) + hmac-md5 OPTIONAL HMAC-MD5 (digest length = key + length = 16) + hmac-md5-96 OPTIONAL first 96 bits of HMAC-MD5 (digest + length = 12, key length = 16) + none OPTIONAL no MAC; NOT RECOMMENDED + + The "hmac-*" algorithms are described in [RFC2104]. The "*-n" MACs + use only the first n bits of the resulting value. + + + + +Ylonen & Lonvick Standards Track [Page 12] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + SHA-1 is described in [FIPS-180-2] and MD5 is described in [RFC1321]. + + Additional methods may be defined, as specified in [SSH-ARCH] and in + [SSH-NUMBERS]. + +6.5. Key Exchange Methods + + The key exchange method specifies how one-time session keys are + generated for encryption and for authentication, and how the server + authentication is done. + + Two REQUIRED key exchange methods have been defined: + + diffie-hellman-group1-sha1 REQUIRED + diffie-hellman-group14-sha1 REQUIRED + + These methods are described in Section 8. + + Additional methods may be defined as specified in [SSH-NUMBERS]. The + name "diffie-hellman-group1-sha1" is used for a key exchange method + using an Oakley group, as defined in [RFC2409]. SSH maintains its + own group identifier space that is logically distinct from Oakley + [RFC2412] and IKE; however, for one additional group, the Working + Group adopted the number assigned by [RFC3526], using diffie- + hellman-group14-sha1 for the name of the second defined group. + Implementations should treat these names as opaque identifiers and + should not assume any relationship between the groups used by SSH and + the groups defined for IKE. + +6.6. Public Key Algorithms + + This protocol has been designed to operate with almost any public key + format, encoding, and algorithm (signature and/or encryption). + + There are several aspects that define a public key type: + + o Key format: how is the key encoded and how are certificates + represented. The key blobs in this protocol MAY contain + certificates in addition to keys. + + o Signature and/or encryption algorithms. Some key types may not + support both signing and encryption. Key usage may also be + restricted by policy statements (e.g., in certificates). In this + case, different key types SHOULD be defined for the different + policy alternatives. + + o Encoding of signatures and/or encrypted data. This includes but + is not limited to padding, byte order, and data formats. + + + +Ylonen & Lonvick Standards Track [Page 13] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + The following public key and/or certificate formats are currently + defined: + + ssh-dss REQUIRED sign Raw DSS Key + ssh-rsa RECOMMENDED sign Raw RSA Key + pgp-sign-rsa OPTIONAL sign OpenPGP certificates (RSA key) + pgp-sign-dss OPTIONAL sign OpenPGP certificates (DSS key) + + Additional key types may be defined, as specified in [SSH-ARCH] and + in [SSH-NUMBERS]. + + The key type MUST always be explicitly known (from algorithm + negotiation or some other source). It is not normally included in + the key blob. + + Certificates and public keys are encoded as follows: + + string certificate or public key format identifier + byte[n] key/certificate data + + The certificate part may be a zero length string, but a public key is + required. This is the public key that will be used for + authentication. The certificate sequence contained in the + certificate blob can be used to provide authorization. + + Public key/certificate formats that do not explicitly specify a + signature format identifier MUST use the public key/certificate + format identifier as the signature identifier. + + Signatures are encoded as follows: + + string signature format identifier (as specified by the + public key/certificate format) + byte[n] signature blob in format specific encoding. + + The "ssh-dss" key format has the following specific encoding: + + string "ssh-dss" + mpint p + mpint q + mpint g + mpint y + + Here, the 'p', 'q', 'g', and 'y' parameters form the signature key + blob. + + + + + + +Ylonen & Lonvick Standards Track [Page 14] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + Signing and verifying using this key format is done according to the + Digital Signature Standard [FIPS-186-2] using the SHA-1 hash + [FIPS-180-2]. + + The resulting signature is encoded as follows: + + string "ssh-dss" + string dss_signature_blob + + The value for 'dss_signature_blob' is encoded as a string containing + r, followed by s (which are 160-bit integers, without lengths or + padding, unsigned, and in network byte order). + + The "ssh-rsa" key format has the following specific encoding: + + string "ssh-rsa" + mpint e + mpint n + + Here the 'e' and 'n' parameters form the signature key blob. + + Signing and verifying using this key format is performed according to + the RSASSA-PKCS1-v1_5 scheme in [RFC3447] using the SHA-1 hash. + + The resulting signature is encoded as follows: + + string "ssh-rsa" + string rsa_signature_blob + + The value for 'rsa_signature_blob' is encoded as a string containing + s (which is an integer, without lengths or padding, unsigned, and in + network byte order). + + The "pgp-sign-rsa" method indicates the certificates, the public key, + and the signature are in OpenPGP compatible binary format + ([RFC2440]). This method indicates that the key is an RSA-key. + + The "pgp-sign-dss" is as above, but indicates that the key is a + DSS-key. + +7. Key Exchange + + Key exchange (kex) begins by each side sending name-lists of + supported algorithms. Each side has a preferred algorithm in each + category, and it is assumed that most implementations, at any given + time, will use the same preferred algorithm. Each side MAY guess + + + + + +Ylonen & Lonvick Standards Track [Page 15] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + which algorithm the other side is using, and MAY send an initial key + exchange packet according to the algorithm, if appropriate for the + preferred method. + + The guess is considered wrong if: + + o the kex algorithm and/or the host key algorithm is guessed wrong + (server and client have different preferred algorithm), or + + o if any of the other algorithms cannot be agreed upon (the + procedure is defined below in Section 7.1). + + Otherwise, the guess is considered to be right, and the + optimistically sent packet MUST be handled as the first key exchange + packet. + + However, if the guess was wrong, and a packet was optimistically sent + by one or both parties, such packets MUST be ignored (even if the + error in the guess would not affect the contents of the initial + packet(s)), and the appropriate side MUST send the correct initial + packet. + + A key exchange method uses explicit server authentication if the key + exchange messages include a signature or other proof of the server's + authenticity. A key exchange method uses implicit server + authentication if, in order to prove its authenticity, the server + also has to prove that it knows the shared secret, K, by sending a + message and a corresponding MAC that the client can verify. + + The key exchange method defined by this document uses explicit server + authentication. However, key exchange methods with implicit server + authentication MAY be used with this protocol. After a key exchange + with implicit server authentication, the client MUST wait for a + response to its service request message before sending any further + data. + + + + + + + + + + + + + + + + +Ylonen & Lonvick Standards Track [Page 16] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + +7.1. Algorithm Negotiation + + Key exchange begins by each side sending the following packet: + + byte SSH_MSG_KEXINIT + byte[16] cookie (random bytes) + name-list kex_algorithms + name-list server_host_key_algorithms + name-list encryption_algorithms_client_to_server + name-list encryption_algorithms_server_to_client + name-list mac_algorithms_client_to_server + name-list mac_algorithms_server_to_client + name-list compression_algorithms_client_to_server + name-list compression_algorithms_server_to_client + name-list languages_client_to_server + name-list languages_server_to_client + boolean first_kex_packet_follows + uint32 0 (reserved for future extension) + + Each of the algorithm name-lists MUST be a comma-separated list of + algorithm names (see Algorithm Naming in [SSH-ARCH] and additional + information in [SSH-NUMBERS]). Each supported (allowed) algorithm + MUST be listed in order of preference, from most to least. + + The first algorithm in each name-list MUST be the preferred (guessed) + algorithm. Each name-list MUST contain at least one algorithm name. + + cookie + The 'cookie' MUST be a random value generated by the sender. + Its purpose is to make it impossible for either side to fully + determine the keys and the session identifier. + + kex_algorithms + Key exchange algorithms were defined above. The first + algorithm MUST be the preferred (and guessed) algorithm. If + both sides make the same guess, that algorithm MUST be used. + Otherwise, the following algorithm MUST be used to choose a key + exchange method: Iterate over client's kex algorithms, one at a + time. Choose the first algorithm that satisfies the following + conditions: + + + the server also supports the algorithm, + + + if the algorithm requires an encryption-capable host key, + there is an encryption-capable algorithm on the server's + server_host_key_algorithms that is also supported by the + client, and + + + + +Ylonen & Lonvick Standards Track [Page 17] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + + if the algorithm requires a signature-capable host key, + there is a signature-capable algorithm on the server's + server_host_key_algorithms that is also supported by the + client. + + If no algorithm satisfying all these conditions can be found, the + connection fails, and both sides MUST disconnect. + + server_host_key_algorithms + A name-list of the algorithms supported for the server host + key. The server lists the algorithms for which it has host + keys; the client lists the algorithms that it is willing to + accept. There MAY be multiple host keys for a host, possibly + with different algorithms. + + Some host keys may not support both signatures and encryption + (this can be determined from the algorithm), and thus not all + host keys are valid for all key exchange methods. + + Algorithm selection depends on whether the chosen key exchange + algorithm requires a signature or an encryption-capable host + key. It MUST be possible to determine this from the public key + algorithm name. The first algorithm on the client's name-list + that satisfies the requirements and is also supported by the + server MUST be chosen. If there is no such algorithm, both + sides MUST disconnect. + + encryption_algorithms + A name-list of acceptable symmetric encryption algorithms (also + known as ciphers) in order of preference. The chosen + encryption algorithm to each direction MUST be the first + algorithm on the client's name-list that is also on the + server's name-list. If there is no such algorithm, both sides + MUST disconnect. + + Note that "none" must be explicitly listed if it is to be + acceptable. The defined algorithm names are listed in Section + 6.3. + + mac_algorithms + A name-list of acceptable MAC algorithms in order of + preference. The chosen MAC algorithm MUST be the first + algorithm on the client's name-list that is also on the + server's name-list. If there is no such algorithm, both sides + MUST disconnect. + + Note that "none" must be explicitly listed if it is to be + acceptable. The MAC algorithm names are listed in Section 6.4. + + + +Ylonen & Lonvick Standards Track [Page 18] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + compression_algorithms + A name-list of acceptable compression algorithms in order of + preference. The chosen compression algorithm MUST be the first + algorithm on the client's name-list that is also on the + server's name-list. If there is no such algorithm, both sides + MUST disconnect. + + Note that "none" must be explicitly listed if it is to be + acceptable. The compression algorithm names are listed in + Section 6.2. + + languages + This is a name-list of language tags in order of preference + [RFC3066]. Both parties MAY ignore this name-list. If there + are no language preferences, this name-list SHOULD be empty as + defined in Section 5 of [SSH-ARCH]. Language tags SHOULD NOT + be present unless they are known to be needed by the sending + party. + + first_kex_packet_follows + Indicates whether a guessed key exchange packet follows. If a + guessed packet will be sent, this MUST be TRUE. If no guessed + packet will be sent, this MUST be FALSE. + + After receiving the SSH_MSG_KEXINIT packet from the other side, + each party will know whether their guess was right. If the + other party's guess was wrong, and this field was TRUE, the + next packet MUST be silently ignored, and both sides MUST then + act as determined by the negotiated key exchange method. If + the guess was right, key exchange MUST continue using the + guessed packet. + + After the SSH_MSG_KEXINIT message exchange, the key exchange + algorithm is run. It may involve several packet exchanges, as + specified by the key exchange method. + + Once a party has sent a SSH_MSG_KEXINIT message for key exchange or + re-exchange, until it has sent a SSH_MSG_NEWKEYS message (Section + 7.3), it MUST NOT send any messages other than: + + o Transport layer generic messages (1 to 19) (but + SSH_MSG_SERVICE_REQUEST and SSH_MSG_SERVICE_ACCEPT MUST NOT be + sent); + + o Algorithm negotiation messages (20 to 29) (but further + SSH_MSG_KEXINIT messages MUST NOT be sent); + + o Specific key exchange method messages (30 to 49). + + + +Ylonen & Lonvick Standards Track [Page 19] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + The provisions of Section 11 apply to unrecognized messages. + + Note, however, that during a key re-exchange, after sending a + SSH_MSG_KEXINIT message, each party MUST be prepared to process an + arbitrary number of messages that may be in-flight before receiving a + SSH_MSG_KEXINIT message from the other party. + +7.2. Output from Key Exchange + + The key exchange produces two values: a shared secret K, and an + exchange hash H. Encryption and authentication keys are derived from + these. The exchange hash H from the first key exchange is + additionally used as the session identifier, which is a unique + identifier for this connection. It is used by authentication methods + as a part of the data that is signed as a proof of possession of a + private key. Once computed, the session identifier is not changed, + even if keys are later re-exchanged. + + Each key exchange method specifies a hash function that is used in + the key exchange. The same hash algorithm MUST be used in key + derivation. Here, we'll call it HASH. + + Encryption keys MUST be computed as HASH, of a known value and K, as + follows: + + o Initial IV client to server: HASH(K || H || "A" || session_id) + (Here K is encoded as mpint and "A" as byte and session_id as raw + data. "A" means the single character A, ASCII 65). + + o Initial IV server to client: HASH(K || H || "B" || session_id) + + o Encryption key client to server: HASH(K || H || "C" || session_id) + + o Encryption key server to client: HASH(K || H || "D" || session_id) + + o Integrity key client to server: HASH(K || H || "E" || session_id) + + o Integrity key server to client: HASH(K || H || "F" || session_id) + + Key data MUST be taken from the beginning of the hash output. As + many bytes as needed are taken from the beginning of the hash value. + If the key length needed is longer than the output of the HASH, the + key is extended by computing HASH of the concatenation of K and H and + the entire key so far, and appending the resulting bytes (as many as + HASH generates) to the key. This process is repeated until enough + key material is available; the key is taken from the beginning of + this value. In other words: + + + + +Ylonen & Lonvick Standards Track [Page 20] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + K1 = HASH(K || H || X || session_id) (X is e.g., "A") + K2 = HASH(K || H || K1) + K3 = HASH(K || H || K1 || K2) + ... + key = K1 || K2 || K3 || ... + + This process will lose entropy if the amount of entropy in K is + larger than the internal state size of HASH. + +7.3. Taking Keys Into Use + + Key exchange ends by each side sending an SSH_MSG_NEWKEYS message. + This message is sent with the old keys and algorithms. All messages + sent after this message MUST use the new keys and algorithms. + + When this message is received, the new keys and algorithms MUST be + used for receiving. + + The purpose of this message is to ensure that a party is able to + respond with an SSH_MSG_DISCONNECT message that the other party can + understand if something goes wrong with the key exchange. + + byte SSH_MSG_NEWKEYS + +8. Diffie-Hellman Key Exchange + + The Diffie-Hellman (DH) key exchange provides a shared secret that + cannot be determined by either party alone. The key exchange is + combined with a signature with the host key to provide host + authentication. This key exchange method provides explicit server + authentication as defined in Section 7. + + The following steps are used to exchange a key. In this, C is the + client; S is the server; p is a large safe prime; g is a generator + for a subgroup of GF(p); q is the order of the subgroup; V_S is S's + identification string; V_C is C's identification string; K_S is S's + public host key; I_C is C's SSH_MSG_KEXINIT message and I_S is S's + SSH_MSG_KEXINIT message that have been exchanged before this part + begins. + + 1. C generates a random number x (1 < x < q) and computes + e = g^x mod p. C sends e to S. + + + + + + + + + +Ylonen & Lonvick Standards Track [Page 21] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + 2. S generates a random number y (0 < y < q) and computes + f = g^y mod p. S receives e. It computes K = e^y mod p, + H = hash(V_C || V_S || I_C || I_S || K_S || e || f || K) + (these elements are encoded according to their types; see below), + and signature s on H with its private host key. S sends + (K_S || f || s) to C. The signing operation may involve a + second hashing operation. + + 3. C verifies that K_S really is the host key for S (e.g., using + certificates or a local database). C is also allowed to accept + the key without verification; however, doing so will render the + protocol insecure against active attacks (but may be desirable for + practical reasons in the short term in many environments). C then + computes K = f^x mod p, H = hash(V_C || V_S || I_C || I_S || K_S + || e || f || K), and verifies the signature s on H. + + Values of 'e' or 'f' that are not in the range [1, p-1] MUST NOT be + sent or accepted by either side. If this condition is violated, the + key exchange fails. + + This is implemented with the following messages. The hash algorithm + for computing the exchange hash is defined by the method name, and is + called HASH. The public key algorithm for signing is negotiated with + the SSH_MSG_KEXINIT messages. + + First, the client sends the following: + + byte SSH_MSG_KEXDH_INIT + mpint e + + The server then responds with the following: + + byte SSH_MSG_KEXDH_REPLY + string server public host key and certificates (K_S) + mpint f + string signature of H + + + + + + + + + + + + + + + +Ylonen & Lonvick Standards Track [Page 22] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + The hash H is computed as the HASH hash of the concatenation of the + following: + + string V_C, the client's identification string (CR and LF + excluded) + string V_S, the server's identification string (CR and LF + excluded) + string I_C, the payload of the client's SSH_MSG_KEXINIT + string I_S, the payload of the server's SSH_MSG_KEXINIT + string K_S, the host key + mpint e, exchange value sent by the client + mpint f, exchange value sent by the server + mpint K, the shared secret + + This value is called the exchange hash, and it is used to + authenticate the key exchange. The exchange hash SHOULD be kept + secret. + + The signature algorithm MUST be applied over H, not the original + data. Most signature algorithms include hashing and additional + padding (e.g., "ssh-dss" specifies SHA-1 hashing). In that case, the + data is first hashed with HASH to compute H, and H is then hashed + with SHA-1 as part of the signing operation. + +8.1. diffie-hellman-group1-sha1 + + The "diffie-hellman-group1-sha1" method specifies the Diffie-Hellman + key exchange with SHA-1 as HASH, and Oakley Group 2 [RFC2409] (1024- + bit MODP Group). This method MUST be supported for interoperability + as all of the known implementations currently support it. Note that + this method is named using the phrase "group1", even though it + specifies the use of Oakley Group 2. + +8.2. diffie-hellman-group14-sha1 + + The "diffie-hellman-group14-sha1" method specifies a Diffie-Hellman + key exchange with SHA-1 as HASH and Oakley Group 14 [RFC3526] (2048- + bit MODP Group), and it MUST also be supported. + +9. Key Re-Exchange + + Key re-exchange is started by sending an SSH_MSG_KEXINIT packet when + not already doing a key exchange (as described in Section 7.1). When + this message is received, a party MUST respond with its own + SSH_MSG_KEXINIT message, except when the received SSH_MSG_KEXINIT + already was a reply. Either party MAY initiate the re-exchange, but + roles MUST NOT be changed (i.e., the server remains the server, and + the client remains the client). + + + +Ylonen & Lonvick Standards Track [Page 23] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + Key re-exchange is performed using whatever encryption was in effect + when the exchange was started. Encryption, compression, and MAC + methods are not changed before a new SSH_MSG_NEWKEYS is sent after + the key exchange (as in the initial key exchange). Re-exchange is + processed identically to the initial key exchange, except for the + session identifier that will remain unchanged. It is permissible to + change some or all of the algorithms during the re-exchange. Host + keys can also change. All keys and initialization vectors are + recomputed after the exchange. Compression and encryption contexts + are reset. + + It is RECOMMENDED that the keys be changed after each gigabyte of + transmitted data or after each hour of connection time, whichever + comes sooner. However, since the re-exchange is a public key + operation, it requires a fair amount of processing power and should + not be performed too often. + + More application data may be sent after the SSH_MSG_NEWKEYS packet + has been sent; key exchange does not affect the protocols that lie + above the SSH transport layer. + +10. Service Request + + After the key exchange, the client requests a service. The service + is identified by a name. The format of names and procedures for + defining new names are defined in [SSH-ARCH] and [SSH-NUMBERS]. + + Currently, the following names have been reserved: + + ssh-userauth + ssh-connection + + Similar local naming policy is applied to the service names, as is + applied to the algorithm names. A local service should use the + PRIVATE USE syntax of "servicename@domain". + + byte SSH_MSG_SERVICE_REQUEST + string service name + + If the server rejects the service request, it SHOULD send an + appropriate SSH_MSG_DISCONNECT message and MUST disconnect. + + When the service starts, it may have access to the session identifier + generated during the key exchange. + + + + + + + +Ylonen & Lonvick Standards Track [Page 24] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + If the server supports the service (and permits the client to use + it), it MUST respond with the following: + + byte SSH_MSG_SERVICE_ACCEPT + string service name + + Message numbers used by services should be in the area reserved for + them (see [SSH-ARCH] and [SSH-NUMBERS]). The transport level will + continue to process its own messages. + + Note that after a key exchange with implicit server authentication, + the client MUST wait for a response to its service request message + before sending any further data. + +11. Additional Messages + + Either party may send any of the following messages at any time. + +11.1. Disconnection Message + + byte SSH_MSG_DISCONNECT + uint32 reason code + string description in ISO-10646 UTF-8 encoding [RFC3629] + string language tag [RFC3066] + + This message causes immediate termination of the connection. All + implementations MUST be able to process this message; they SHOULD be + able to send this message. + + The sender MUST NOT send or receive any data after this message, and + the recipient MUST NOT accept any data after receiving this message. + The Disconnection Message 'description' string gives a more specific + explanation in a human-readable form. The Disconnection Message + 'reason code' gives the reason in a more machine-readable format + (suitable for localization), and can have the values as displayed in + the table below. Note that the decimal representation is displayed + in this table for readability, but the values are actually uint32 + values. + + + + + + + + + + + + + +Ylonen & Lonvick Standards Track [Page 25] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + Symbolic name reason code + ------------- ----------- + SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT 1 + SSH_DISCONNECT_PROTOCOL_ERROR 2 + SSH_DISCONNECT_KEY_EXCHANGE_FAILED 3 + SSH_DISCONNECT_RESERVED 4 + SSH_DISCONNECT_MAC_ERROR 5 + SSH_DISCONNECT_COMPRESSION_ERROR 6 + SSH_DISCONNECT_SERVICE_NOT_AVAILABLE 7 + SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED 8 + SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE 9 + SSH_DISCONNECT_CONNECTION_LOST 10 + SSH_DISCONNECT_BY_APPLICATION 11 + SSH_DISCONNECT_TOO_MANY_CONNECTIONS 12 + SSH_DISCONNECT_AUTH_CANCELLED_BY_USER 13 + SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE 14 + SSH_DISCONNECT_ILLEGAL_USER_NAME 15 + + If the 'description' string is displayed, the control character + filtering discussed in [SSH-ARCH] should be used to avoid attacks by + sending terminal control characters. + + Requests for assignments of new Disconnection Message 'reason code' + values (and associated 'description' text) in the range of 0x00000010 + to 0xFDFFFFFF MUST be done through the IETF CONSENSUS method, as + described in [RFC2434]. The Disconnection Message 'reason code' + values in the range of 0xFE000000 through 0xFFFFFFFF are reserved for + PRIVATE USE. As noted, the actual instructions to the IANA are in + [SSH-NUMBERS]. + +11.2. Ignored Data Message + + byte SSH_MSG_IGNORE + string data + + All implementations MUST understand (and ignore) this message at any + time (after receiving the identification string). No implementation + is required to send them. This message can be used as an additional + protection measure against advanced traffic analysis techniques. + +11.3. Debug Message + + byte SSH_MSG_DEBUG + boolean always_display + string message in ISO-10646 UTF-8 encoding [RFC3629] + string language tag [RFC3066] + + + + + +Ylonen & Lonvick Standards Track [Page 26] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + All implementations MUST understand this message, but they are + allowed to ignore it. This message is used to transmit information + that may help debugging. If 'always_display' is TRUE, the message + SHOULD be displayed. Otherwise, it SHOULD NOT be displayed unless + debugging information has been explicitly requested by the user. + + The 'message' doesn't need to contain a newline. It is, however, + allowed to consist of multiple lines separated by CRLF (Carriage + Return - Line Feed) pairs. + + If the 'message' string is displayed, the terminal control character + filtering discussed in [SSH-ARCH] should be used to avoid attacks by + sending terminal control characters. + +11.4. Reserved Messages + + An implementation MUST respond to all unrecognized messages with an + SSH_MSG_UNIMPLEMENTED message in the order in which the messages were + received. Such messages MUST be otherwise ignored. Later protocol + versions may define other meanings for these message types. + + byte SSH_MSG_UNIMPLEMENTED + uint32 packet sequence number of rejected message + +12. Summary of Message Numbers + + The following is a summary of messages and their associated message + number. + + SSH_MSG_DISCONNECT 1 + SSH_MSG_IGNORE 2 + SSH_MSG_UNIMPLEMENTED 3 + SSH_MSG_DEBUG 4 + SSH_MSG_SERVICE_REQUEST 5 + SSH_MSG_SERVICE_ACCEPT 6 + SSH_MSG_KEXINIT 20 + SSH_MSG_NEWKEYS 21 + + Note that numbers 30-49 are used for kex packets. Different kex + methods may reuse message numbers in this range. + +13. IANA Considerations + + This document is part of a set. The IANA considerations for the SSH + protocol as defined in [SSH-ARCH], [SSH-USERAUTH], [SSH-CONNECT], and + this document, are detailed in [SSH-NUMBERS]. + + + + + +Ylonen & Lonvick Standards Track [Page 27] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + +14. Security Considerations + + This protocol provides a secure encrypted channel over an insecure + network. It performs server host authentication, key exchange, + encryption, and integrity protection. It also derives a unique + session ID that may be used by higher-level protocols. + + Full security considerations for this protocol are provided in + [SSH-ARCH]. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Ylonen & Lonvick Standards Track [Page 28] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + +15. References + +15.1. Normative References + + [SSH-ARCH] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell + (SSH) Protocol Architecture", RFC 4251, January 2006. + + [SSH-USERAUTH] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell + (SSH) Authentication Protocol", RFC 4252, January + 2006. + + [SSH-CONNECT] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell + (SSH) Connection Protocol", RFC 4254, January 2006. + + [SSH-NUMBERS] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell + (SSH) Protocol Assigned Numbers", RFC 4250, January + 2006. + + [RFC1321] Rivest, R., "The MD5 Message-Digest Algorithm ", RFC + 1321, April 1992. + + [RFC1950] Deutsch, P. and J-L. Gailly, "ZLIB Compressed Data + Format Specification version 3.3", RFC 1950, May 1996. + + [RFC1951] Deutsch, P., "DEFLATE Compressed Data Format + Specification version 1.3", RFC 1951, May 1996. + + [RFC2104] Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: + Keyed-Hashing for Message Authentication", RFC 2104, + February 1997. + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997. + + [RFC2144] Adams, C., "The CAST-128 Encryption Algorithm", RFC + 2144, May 1997. + + [RFC2409] Harkins, D. and D. Carrel, "The Internet Key Exchange + (IKE)", RFC 2409, November 1998. + + [RFC2434] Narten, T. and H. Alvestrand, "Guidelines for Writing + an IANA Considerations Section in RFCs", BCP 26, RFC + 2434, October 1998. + + [RFC2440] Callas, J., Donnerhacke, L., Finney, H., and R. + Thayer, "OpenPGP Message Format", RFC 2440, November + 1998. + + + + +Ylonen & Lonvick Standards Track [Page 29] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + [RFC3066] Alvestrand, H., "Tags for the Identification of + Languages", BCP 47, RFC 3066, January 2001. + + [RFC3447] Jonsson, J. and B. Kaliski, "Public-Key Cryptography + Standards (PKCS) #1: RSA Cryptography Specifications + Version 2.1", RFC 3447, February 2003. + + [RFC3526] Kivinen, T. and M. Kojo, "More Modular Exponential + (MODP) Diffie-Hellman groups for Internet Key Exchange + (IKE)", RFC 3526, May 2003. + + [RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO + 10646", STD 63, RFC 3629, November 2003. + + [FIPS-180-2] US National Institute of Standards and Technology, + "Secure Hash Standard (SHS)", Federal Information + Processing Standards Publication 180-2, August 2002. + + [FIPS-186-2] US National Institute of Standards and Technology, + "Digital Signature Standard (DSS)", Federal + Information Processing Standards Publication 186-2, + January 2000. + + [FIPS-197] US National Institute of Standards and Technology, + "Advanced Encryption Standard (AES)", Federal + Information Processing Standards Publication 197, + November 2001. + + [FIPS-46-3] US National Institute of Standards and Technology, + "Data Encryption Standard (DES)", Federal Information + Processing Standards Publication 46-3, October 1999. + + [SCHNEIER] Schneier, B., "Applied Cryptography Second Edition: + protocols algorithms and source in code in C", John + Wiley and Sons, New York, NY, 1996. + + [TWOFISH] Schneier, B., "The Twofish Encryptions Algorithm: A + 128-Bit Block Cipher, 1st Edition", March 1999. + +15.2. Informative References + + [RFC0894] Hornig, C., "Standard for the transmission of IP + datagrams over Ethernet networks", STD 41, RFC 894, + April 1984. + + [RFC1661] Simpson, W., "The Point-to-Point Protocol (PPP)", STD + 51, RFC 1661, July 1994. + + + + +Ylonen & Lonvick Standards Track [Page 30] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + + [RFC2412] Orman, H., "The OAKLEY Key Determination Protocol", + RFC 2412, November 1998. + + [ssh-1.2.30] Ylonen, T., "ssh-1.2.30/RFC", File within compressed + tarball ftp://ftp.funet.fi/pub/unix/security/ + login/ssh/ssh-1.2.30.tar.gz, November 1995. + +Authors' Addresses + + Tatu Ylonen + SSH Communications Security Corp + Valimotie 17 + 00380 Helsinki + Finland + + EMail: ylo@ssh.com + + + Chris Lonvick (editor) + Cisco Systems, Inc. + 12515 Research Blvd. + Austin 78759 + USA + + EMail: clonvick@cisco.com + +Trademark Notice + + "ssh" is a registered trademark in the United States and/or other + countries. + + + + + + + + + + + + + + + + + + + + + +Ylonen & Lonvick Standards Track [Page 31] + +RFC 4253 SSH Transport Layer Protocol January 2006 + + +Full Copyright Statement + + Copyright (C) The Internet Society (2006). + + This document is subject to the rights, licenses and restrictions + contained in BCP 78, and except as set forth therein, the authors + retain all their rights. + + This document and the information contained herein are provided on an + "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS + OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY AND THE INTERNET + ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE + INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED + WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +Intellectual Property + + The IETF takes no position regarding the validity or scope of any + Intellectual Property Rights or other rights that might be claimed to + pertain to the implementation or use of the technology described in + this document or the extent to which any license under such rights + might or might not be available; nor does it represent that it has + made any independent effort to identify any such rights. Information + on the procedures with respect to rights in RFC documents can be + found in BCP 78 and BCP 79. + + Copies of IPR disclosures made to the IETF Secretariat and any + assurances of licenses to be made available, or the result of an + attempt made to obtain a general license or permission for the use of + such proprietary rights by implementers or users of this + specification can be obtained from the IETF on-line IPR repository at + http://www.ietf.org/ipr. + + The IETF invites any interested party to bring to its attention any + copyrights, patents or patent applications, or other proprietary + rights that may cover technology that may be required to implement + this standard. Please address the information to the IETF at + ietf-ipr@ietf.org. + +Acknowledgement + + Funding for the RFC Editor function is provided by the IETF + Administrative Support Activity (IASA). + + + + + + + +Ylonen & Lonvick Standards Track [Page 32] + diff --git a/specifications/rfc6668.txt b/specifications/rfc6668.txt new file mode 100644 index 0000000..18c4d67 --- /dev/null +++ b/specifications/rfc6668.txt @@ -0,0 +1,283 @@ + + + + + + +Internet Engineering Task Force (IETF) D. Bider +Request for Comments: 6668 Bitvise Limited +Updates: 4253 M. Baushke +Category: Standards Track Juniper Networks, Inc. +ISSN: 2070-1721 July 2012 + + + SHA-2 Data Integrity Verification for + the Secure Shell (SSH) Transport Layer Protocol + +Abstract + + This memo defines algorithm names and parameters for use in some of + the SHA-2 family of secure hash algorithms for data integrity + verification in the Secure Shell (SSH) protocol. It also updates RFC + 4253 by specifying a new RECOMMENDED data integrity algorithm. + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 5741. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + http://www.rfc-editor.org/info/rfc6668. + +Copyright Notice + + Copyright (c) 2012 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + + + + + + +Bider & Baushke Standards Track [Page 1] + +RFC 6668 Sha2-Transport Layer Protocol July 2012 + + +1. Overview and Rationale + + The Secure Shell (SSH) [RFC4251] is a very common protocol for secure + remote login on the Internet. Currently, SSH defines data integrity + verification using SHA-1 and MD5 algorithms [RFC4253]. Due to recent + security concerns with these two algorithms ([RFC6194] and [RFC6151], + respectively), implementors and users request support for data + integrity verification using some of the SHA-2 family of secure hash + algorithms. + +1.1. Requirements Terminology + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in [RFC2119]. + +2. Data Integrity Algorithms + + This memo adopts the style and conventions of [RFC4253] in specifying + how the use of new data integrity algorithms are indicated in SSH. + + The following new data integrity algorithms are defined: + + hmac-sha2-256 RECOMMENDED HMAC-SHA2-256 + (digest length = 32 bytes, + key length = 32 bytes) + + hmac-sha2-512 OPTIONAL HMAC-SHA2-512 + (digest length = 64 bytes, + key length = 64 bytes) + + Figure 1 + + The Hashed Message Authentication Code (HMAC) mechanism was + originally defined in [RFC2104] and has been updated in [RFC6151]. + + The SHA-2 family of secure hash algorithms is defined in + [FIPS-180-3]. + + Sample code for the SHA-based HMAC algorithms are available in + [RFC6234]. The variants, HMAC-SHA2-224 and HMAC-SHA2-384 algorithms, + were considered but not added to this list as they have the same + computational requirements of HMAC-SHA2-256 and HMAC-SHA2-512, + respectively, and do not seem to be much used in practice. + + + + + + + +Bider & Baushke Standards Track [Page 2] + +RFC 6668 Sha2-Transport Layer Protocol July 2012 + + + Test vectors for use of HMAC with SHA-2 are provided in [RFC4231]. + Users, implementors, and administrators may choose to put these new + MACs into the proposal ahead of the REQUIRED hmac-sha1 algorithm + defined in [RFC4253] so that they are negotiated first. + +3. IANA Considerations + + This document augments the MAC Algorithm Names in [RFC4253] and + [RFC4250]. + + IANA has updated the "Secure Shell (SSH) Protocol Parameters" + registry with the following entries: + + MAC Algorithm Name Reference Note + hmac-sha2-256 RFC 6668 Section 2 + hmac-sha2-512 RFC 6668 Section 2 + + Figure 2 + +4. Security Considerations + + The security considerations of RFC 4253 [RFC4253] apply to this + document. + + The National Institute of Standards and Technology (NIST) + publications: NIST Special Publication (SP) 800-107 [800-107] and + NIST SP 800-131A [800-131A] suggest that HMAC-SHA1 and HMAC-SHA2-256 + have a security strength of 128 bits and 256 bits, respectively, + which are considered acceptable key lengths. + + Many users seem to be interested in the perceived safety of using the + SHA2-based algorithms for hashing. + +5. References + +5.1. Normative References + + [FIPS-180-3] + National Institute of Standards and Technology (NIST), + United States of America, "Secure Hash Standard (SHS)", + FIPS PUB 180-3, October 2008, . + + [RFC2104] Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed- + Hashing for Message Authentication", RFC 2104, February + 1997. + + + + + +Bider & Baushke Standards Track [Page 3] + +RFC 6668 Sha2-Transport Layer Protocol July 2012 + + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997. + + [RFC4231] Nystrom, M., "Identifiers and Test Vectors for HMAC- + SHA-224, HMAC-SHA-256, HMAC-SHA-384, and HMAC-SHA-512", + RFC 4231, December 2005. + + [RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Transport Layer Protocol", RFC 4253, January 2006. + +5.2. Informative References + + [800-107] National Institute of Standards and Technology (NIST), + "Recommendation for Applications Using Approved Hash + Algorithms", NIST Special Publication 800-107, February + 2009, . + + [800-131A] National Institute of Standards and Technology (NIST), + "Transitions: Recommendation for the Transitioning of the + Use of Cryptographic Algorithms and Key Lengths", DRAFT + NIST Special Publication 800-131A, January 2011, + . + + [RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH) + Protocol Assigned Numbers", RFC 4250, January 2006. + + [RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Protocol Architecture", RFC 4251, January 2006. + + [RFC6151] Turner, S. and L. Chen, "Updated Security Considerations + for the MD5 Message-Digest and the HMAC-MD5 Algorithms", + RFC 6151, March 2011. + + [RFC6194] Polk, T., Chen, L., Turner, S., and P. Hoffman, "Security + Considerations for the SHA-0 and SHA-1 Message-Digest + Algorithms", RFC 6194, March 2011. + + [RFC6234] Eastlake 3rd, D. and T. Hansen, "US Secure Hash Algorithms + (SHA and SHA-based HMAC and HKDF)", RFC 6234, May 2011. + + + + + + + + + + +Bider & Baushke Standards Track [Page 4] + +RFC 6668 Sha2-Transport Layer Protocol July 2012 + + +Authors' Addresses + + Denis Bider + Bitvise Limited + Suites 41/42, Victoria House + 26 Main Street + GI + + Phone: +1 869 762 1410 + EMail: ietf-ssh2@denisbider.com + URI: http://www.bitvise.com/ + + + Mark D. Baushke + Juniper Networks, Inc. + 1194 N Mathilda Av + Sunnyvale, CA 94089-1206 + US + + Phone: +1 408 745 2952 + EMail: mdb@juniper.net + URI: http://www.juniper.net/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Bider & Baushke Standards Track [Page 5] + diff --git a/specifications/rfc8308.txt b/specifications/rfc8308.txt new file mode 100644 index 0000000..eb29130 --- /dev/null +++ b/specifications/rfc8308.txt @@ -0,0 +1,787 @@ + + + + + + +Internet Engineering Task Force (IETF) D. Bider +Request for Comments: 8308 Bitvise Limited +Updates: 4251, 4252, 4253, 4254 March 2018 +Category: Standards Track +ISSN: 2070-1721 + + + Extension Negotiation in the Secure Shell (SSH) Protocol + +Abstract + + This memo updates RFCs 4251, 4252, 4253, and 4254 by defining a + mechanism for Secure Shell (SSH) clients and servers to exchange + information about supported protocol extensions confidentially after + SSH key exchange. + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 7841. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + https://www.rfc-editor.org/info/rfc8308. + +Copyright Notice + + Copyright (c) 2018 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (https://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + + + + + + + +Bider Standards Track [Page 1] + +RFC 8308 Extension Negotiation in SSH March 2018 + + +Table of Contents + + 1. Overview and Rationale ..........................................3 + 1.1. Requirements Terminology ...................................3 + 1.2. Wire Encoding Terminology ..................................3 + 2. Extension Negotiation Mechanism .................................3 + 2.1. Signaling of Extension Negotiation in SSH_MSG_KEXINIT ......3 + 2.2. Enabling Criteria ..........................................4 + 2.3. SSH_MSG_EXT_INFO Message ...................................4 + 2.4. Message Order ..............................................5 + 2.5. Interpretation of Extension Names and Values ...............6 + 3. Initially Defined Extensions ....................................6 + 3.1. "server-sig-algs" ..........................................6 + 3.2. "delay-compression" ........................................7 + 3.2.1. Awkwardly Timed Key Re-Exchange .....................9 + 3.2.2. Subsequent Re-Exchange ..............................9 + 3.2.3. Compatibility Note: OpenSSH up to Version 7.5 .......9 + 3.3. "no-flow-control" .........................................10 + 3.3.1. Prior "No Flow Control" Practice ...................10 + 3.4. "elevation" ...............................................11 + 4. IANA Considerations ............................................12 + 4.1. Additions to Existing Registries ..........................12 + 4.2. New Registry: Extension Names .............................12 + 4.2.1. Future Assignments to Extension Names Registry .....12 + 5. Security Considerations ........................................12 + 6. References .....................................................13 + 6.1. Normative References ......................................13 + 6.2. Informative References ....................................13 + Acknowledgments ...................................................14 + Author's Address ..................................................14 + + + + + + + + + + + + + + + + + + + + + +Bider Standards Track [Page 2] + +RFC 8308 Extension Negotiation in SSH March 2018 + + +1. Overview and Rationale + + Secure Shell (SSH) is a common protocol for secure communication on + the Internet. The original design of the SSH transport layer + [RFC4253] lacks proper extension negotiation. Meanwhile, diverse + implementations take steps to ensure that known message types contain + no unrecognized information. This makes it difficult for + implementations to signal capabilities and negotiate extensions + without risking disconnection. This obstacle has been recognized in + the process of updating SSH to support RSA signatures using SHA-256 + and SHA-512 [RFC8332]. To avoid trial and error as well as + authentication penalties, a client must be able to discover public + key algorithms a server accepts. This extension mechanism permits + this discovery. + + This memo updates RFCs 4251, 4252, 4253, and 4254. + +1.1. Requirements Terminology + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + +1.2. Wire Encoding Terminology + + The wire encoding types in this document -- "byte", "uint32", + "string", "boolean", "name-list" -- have meanings as described in + [RFC4251]. + +2. Extension Negotiation Mechanism + +2.1. Signaling of Extension Negotiation in SSH_MSG_KEXINIT + + Applications implementing this mechanism MUST add one of the + following indicator names to the field kex_algorithms in the + SSH_MSG_KEXINIT message sent by the application in the first key + exchange: + + o When acting as server: "ext-info-s" + + o When acting as client: "ext-info-c" + + The indicator name is added without quotes and MAY be added at any + position in the name-list, subject to proper separation from other + names as per name-list conventions. + + + + +Bider Standards Track [Page 3] + +RFC 8308 Extension Negotiation in SSH March 2018 + + + The names are added to the kex_algorithms field because this is one + of two name-list fields in SSH_MSG_KEXINIT that do not have a + separate copy for each data direction. + + The indicator names inserted by the client and server are different + to ensure these names will not produce a match and therefore not + affect the algorithm chosen in key exchange algorithm negotiation. + + The inclusion of textual indicator names is intended to provide a + clue for implementers to discover this mechanism. + +2.2. Enabling Criteria + + If a client or server offers "ext-info-c" or "ext-info-s" + respectively, it MUST be prepared to accept an SSH_MSG_EXT_INFO + message from the peer. + + A server only needs to send "ext-info-s" if it intends to process + SSH_MSG_EXT_INFO from the client. A client only needs to send + "ext-info-c" if it plans to process SSH_MSG_EXT_INFO from the server. + + If a server receives an "ext-info-c", or a client receives an + "ext-info-s", it MAY send an SSH_MSG_EXT_INFO message but is not + required to do so. + + Neither party needs to wait for the other's SSH_MSG_KEXINIT in order + to decide whether to send the appropriate indicator in its own + SSH_MSG_KEXINIT. + + Implementations MUST NOT send an incorrect indicator name for their + role. Implementations MAY disconnect if the counterparty sends an + incorrect indicator. If "ext-info-c" or "ext-info-s" ends up being + negotiated as a key exchange method, the parties MUST disconnect. + +2.3. SSH_MSG_EXT_INFO Message + + A party that received the "ext-info-c" or "ext-info-s" indicator MAY + send the following message: + + byte SSH_MSG_EXT_INFO (value 7) + uint32 nr-extensions + repeat the following 2 fields "nr-extensions" times: + string extension-name + string extension-value (binary) + + + + + + + +Bider Standards Track [Page 4] + +RFC 8308 Extension Negotiation in SSH March 2018 + + + Implementers should pay careful attention to Section 2.5, in + particular to the requirement to tolerate any sequence of bytes + (including null bytes at any position) in an unknown extension's + extension-value. + +2.4. Message Order + + If a client sends SSH_MSG_EXT_INFO, it MUST send it as the next + packet following the client's first SSH_MSG_NEWKEYS message to the + server. + + If a server sends SSH_MSG_EXT_INFO, it MAY send it at zero, one, or + both of the following opportunities: + + o As the next packet following the server's first SSH_MSG_NEWKEYS. + + Where clients need information in the server's SSH_MSG_EXT_INFO to + authenticate, it is helpful if the server sends its + SSH_MSG_EXT_INFO not only as the next packet after + SSH_MSG_NEWKEYS, but without delay. + + Clients cannot rely on this because the server is not required to + send the message at this time; if sent, it may be delayed by the + network. However, if a timely SSH_MSG_EXT_INFO is received, a + client can pipeline an authentication request after its + SSH_MSG_SERVICE_REQUEST, even when it needs extension information. + + o Immediately preceding the server's SSH_MSG_USERAUTH_SUCCESS, as + defined in [RFC4252]. + + The server MAY send SSH_MSG_EXT_INFO at this second opportunity, + whether or not it sent it at the first. A client that sent + "ext-info-c" MUST accept a server's SSH_MSG_EXT_INFO at both + opportunities but MUST NOT require it. + + This allows a server to reveal support for additional extensions + that it was unwilling to reveal to an unauthenticated client. If + a server sends a second SSH_MSG_EXT_INFO, this replaces any + initial one, and both the client and the server re-evaluate + extensions in effect. The server's second SSH_MSG_EXT_INFO is + matched against the client's original. + + The timing of the second opportunity is chosen for the following + reasons. If the message was sent earlier, it would not allow the + server to withhold information until the client has authenticated. + If it was sent later, a client that needs information from the + second SSH_MSG_EXT_INFO immediately after it authenticates would + have no way to reliably know whether to expect the message. + + + +Bider Standards Track [Page 5] + +RFC 8308 Extension Negotiation in SSH March 2018 + + +2.5. Interpretation of Extension Names and Values + + Each extension is identified by its extension-name and defines the + conditions under which the extension is considered to be in effect. + Applications MUST ignore unrecognized extension-names. + + When it is specified, an extension MAY dictate that, in order to take + effect, both parties must include it in their SSH_MSG_EXT_INFO or + that it is sufficient for only one party to include it. However, + other rules MAY be specified. The relative order in which extensions + appear in an SSH_MSG_EXT_INFO message MUST be ignored. + + Extension-value fields are interpreted as defined by their respective + extension. This field MAY be empty if permitted by the extension. + Applications that do not implement or recognize an extension MUST + ignore its extension-value, regardless of its size or content. + Applications MUST tolerate any sequence of bytes -- including null + bytes at any position -- in an unknown extension's extension-value. + + The cumulative size of an SSH_MSG_EXT_INFO message is limited only by + the maximum packet length that an implementation may apply in + accordance with [RFC4253]. Implementations MUST accept well-formed + SSH_MSG_EXT_INFO messages up to the maximum packet length they + accept. + +3. Initially Defined Extensions + +3.1. "server-sig-algs" + + This extension is sent with the following extension name and value: + + string "server-sig-algs" + name-list public-key-algorithms-accepted + + The name-list type is a strict subset of the string type and is thus + permissible as an extension-value. See [RFC4251] for more + information. + + This extension is sent by the server and contains a list of public + key algorithms that the server is able to process as part of a + "publickey" authentication request. If a client sends this + extension, the server MAY ignore it and MAY disconnect. + + In this extension, a server MUST enumerate all public key algorithms + it might accept during user authentication. However, early server + implementations that do not enumerate all accepted algorithms do + + + + + +Bider Standards Track [Page 6] + +RFC 8308 Extension Negotiation in SSH March 2018 + + + exist. For this reason, a client MAY send a user authentication + request using a public key algorithm not included in "server-sig- + algs". + + A client that wishes to proceed with public key authentication MAY + wait for the server's SSH_MSG_EXT_INFO so it can send a "publickey" + authentication request with an appropriate public key algorithm, + rather than resorting to trial and error. + + Servers that implement public key authentication SHOULD implement + this extension. + + If a server does not send this extension, a client MUST NOT make any + assumptions about the server's public key algorithm support, and MAY + proceed with authentication requests using trial and error. Note + that implementations are known to exist that apply authentication + penalties if the client attempts to use an unexpected public key + algorithm. + + Authentication penalties are applied by servers to deter brute-force + password guessing, username enumeration, and other types of behavior + deemed suspicious by server administrators or implementers. + Penalties may include automatic IP address throttling or blocking, + and they may trigger email alerts or auditing. + +3.2. "delay-compression" + + This extension MAY be sent by both parties as follows: + + string "delay-compression" + string: + name-list compression_algorithms_client_to_server + name-list compression_algorithms_server_to_client + + The extension-value is a string that encodes two name-lists. The + name-lists themselves have the encoding of strings. For example, to + indicate a preference for algorithms "foo,bar" in the client-to- + server direction and "bar,baz" in the server-to-client direction, a + sender encodes the extension-value as follows (including its length): + + 00000016 00000007 666f6f2c626172 00000007 6261722c62617a + + This same encoding could be sent by either party -- client or server. + + This extension allows the server and client to renegotiate + compression algorithm support without having to conduct a key + re-exchange, which puts new algorithms into effect immediately upon + successful authentication. + + + +Bider Standards Track [Page 7] + +RFC 8308 Extension Negotiation in SSH March 2018 + + + This extension takes effect only if both parties send it. Name-lists + MAY include any compression algorithm that could have been negotiated + in SSH_MSG_KEXINIT, except algorithms that define their own delayed + compression semantics. This means "zlib,none" is a valid algorithm + list in this context, but "zlib@openssh.com" is not. + + If both parties send this extension, but the name-lists do not + contain a common algorithm in either direction, the parties MUST + disconnect in the same way as if negotiation failed as part of + SSH_MSG_KEXINIT. + + If this extension takes effect, the renegotiated compression + algorithm is activated for the very next SSH message after the + trigger message: + + o Sent by the server, the trigger message is + SSH_MSG_USERAUTH_SUCCESS. + + o Sent by the client, the trigger message is SSH_MSG_NEWCOMPRESS. + + If this extension takes effect, the client MUST send the following + message within a reasonable number of outgoing SSH messages after + receiving SSH_MSG_USERAUTH_SUCCESS, but not necessarily as the first + such outgoing message: + + byte SSH_MSG_NEWCOMPRESS (value 8) + + The purpose of SSH_MSG_NEWCOMPRESS is to avoid a race condition where + the server cannot reliably know whether a message sent by the client + was sent before or after receiving the server's + SSH_MSG_USERAUTH_SUCCESS. For example, clients may send keep-alive + messages during logon processing. + + As is the case for all extensions unless otherwise noted, the server + MAY delay including this extension until its secondary + SSH_MSG_EXT_INFO, sent before SSH_MSG_USERAUTH_SUCCESS. This allows + the server to avoid advertising compression until the client has + authenticated. + + If the parties renegotiate compression using this extension in a + session where compression is already enabled and the renegotiated + algorithm is the same in one or both directions, then the internal + compression state MUST be reset for each direction at the time the + renegotiated algorithm takes effect. + + + + + + + +Bider Standards Track [Page 8] + +RFC 8308 Extension Negotiation in SSH March 2018 + + +3.2.1. Awkwardly Timed Key Re-Exchange + + A party that has signaled, or intends to signal, support for this + extension in an SSH session MUST NOT initiate key re-exchange in that + session until either of the following occurs: + + o This extension was negotiated, and the party that's about to start + key re-exchange already sent its trigger message for compression. + + o The party has sent (if server) or received (if client) the message + SSH_MSG_USERAUTH_SUCCESS, and this extension was not negotiated. + + If a party violates this rule, the other party MAY disconnect. + + In general, parties SHOULD NOT start key re-exchange before + successful user authentication but MAY tolerate it if not using this + extension. + +3.2.2. Subsequent Re-Exchange + + In subsequent key re-exchanges that unambiguously begin after the + compression trigger messages, the compression algorithms negotiated + in re-exchange override the algorithms negotiated with this + extension. + +3.2.3. Compatibility Note: OpenSSH up to Version 7.5 + + This extension uses a binary extension-value encoding. OpenSSH + clients up to and including version 7.5 advertise support to receive + SSH_MSG_EXT_INFO but disconnect on receipt of an extension-value + containing null bytes. This is an error fixed in OpenSSH + version 7.6. + + Implementations that wish to interoperate with OpenSSH 7.5 and + earlier are advised to check the remote party's SSH version string + and omit this extension if an affected version is detected. Affected + versions do not implement this extension, so there is no harm in + omitting it. The extension SHOULD NOT be omitted if the detected + OpenSSH version is 7.6 or higher. This would make it harder for the + OpenSSH project to implement this extension in a higher version. + + + + + + + + + + + +Bider Standards Track [Page 9] + +RFC 8308 Extension Negotiation in SSH March 2018 + + +3.3. "no-flow-control" + + This extension is sent with the following extension name and value: + + string "no-flow-control" + string choice of: "p" for preferred | "s" for supported + + A party SHOULD send "s" if it supports "no-flow-control" but does not + prefer to enable it. A party SHOULD send "p" if it prefers to enable + the extension if the other party supports it. Parties MAY disconnect + if they receive a different extension value. + + For this extension to take effect, the following must occur: + + o This extension MUST be sent by both parties. + + o At least one party MUST have sent the value "p" (preferred). + + If this extension takes effect, the "initial window size" fields in + SSH_MSG_CHANNEL_OPEN and SSH_MSG_CHANNEL_OPEN_CONFIRMATION, as + defined in [RFC4254], become meaningless. The values of these fields + MUST be ignored, and a channel behaves as if all window sizes are + infinite. Neither side is required to send any + SSH_MSG_CHANNEL_WINDOW_ADJUST messages, and if received, such + messages MUST be ignored. + + This extension is intended for, but not limited to, use by file + transfer applications that are only going to use one channel and for + which the flow control provided by SSH is an impediment, rather than + a feature. + + Implementations MUST refuse to open more than one simultaneous + channel when this extension is in effect. Nevertheless, server + implementations SHOULD support clients opening more than one + non-simultaneous channel. + +3.3.1. Prior "No Flow Control" Practice + + Before this extension, some applications would simply not implement + SSH flow control, sending an initial channel window size of 2^32 - 1. + Applications SHOULD NOT do this for the following reasons: + + o It is plausible to transfer more than 2^32 bytes over a channel. + Such a channel will hang if the other party implements SSH flow + control according to [RFC4254]. + + + + + + +Bider Standards Track [Page 10] + +RFC 8308 Extension Negotiation in SSH March 2018 + + + o Implementations that cannot handle large channel window sizes + exist, and they can exhibit non-graceful behaviors, including + disconnect. + +3.4. "elevation" + + The terms "elevation" and "elevated" refer to an operating system + mechanism where an administrator's logon session is associated with + two security contexts: one limited and one with administrative + rights. To "elevate" such a session is to activate the security + context with full administrative rights. For more information about + this mechanism on Windows, see [WINADMIN] and [WINTOKEN]. + + This extension MAY be sent by the client as follows: + + string "elevation" + string choice of: "y" | "n" | "d" + + A client sends "y" to indicate its preference that the session should + be elevated; "n" to not be elevated; and "d" for the server to use + its default behavior. The server MAY disconnect if it receives a + different extension value. If a client does not send the "elevation" + extension, the server SHOULD act as if "d" was sent. + + If a client has included this extension, then after authentication, a + server that supports this extension SHOULD indicate to the client + whether elevation was done by sending the following global request: + + byte SSH_MSG_GLOBAL_REQUEST + string "elevation" + boolean want reply = false + boolean elevation performed + + Clients that implement this extension help reduce attack surface for + Windows servers that handle administrative logins. Where clients do + not support this extension, servers must elevate sessions to allow + full access by administrative users always. Where clients support + this extension, sessions can be created without elevation unless + requested. + + + + + + + + + + + + +Bider Standards Track [Page 11] + +RFC 8308 Extension Negotiation in SSH March 2018 + + +4. IANA Considerations + +4.1. Additions to Existing Registries + + IANA has added the following entries to the "Message Numbers" + registry [IANA-M] under the "Secure Shell (SSH) Protocol Parameters" + registry [RFC4250]: + + Value Message ID Reference + ----------------------------------------- + 7 SSH_MSG_EXT_INFO RFC 8308 + 8 SSH_MSG_NEWCOMPRESS RFC 8308 + + IANA has also added the following entries to the "Key Exchange Method + Names" registry [IANA-KE]: + + Method Name Reference Note + ------------------------------------------ + ext-info-s RFC 8308 Section 2 + ext-info-c RFC 8308 Section 2 + +4.2. New Registry: Extension Names + + Also under the "Secure Shell (SSH) Protocol Parameters" registry, + IANA has created a new "Extension Names" registry, with the following + initial content: + + Extension Name Reference Note + ------------------------------------------------ + server-sig-algs RFC 8308 Section 3.1 + delay-compression RFC 8308 Section 3.2 + no-flow-control RFC 8308 Section 3.3 + elevation RFC 8308 Section 3.4 + +4.2.1. Future Assignments to Extension Names Registry + + Names in the "Extension Names" registry MUST follow the conventions + for names defined in [RFC4250], Section 4.6.1. + + Requests for assignments of new non-local names in the "Extension + Names" registry (i.e., names not including the '@' character) MUST be + done using the IETF Review policy, as described in [RFC8126]. + +5. Security Considerations + + Security considerations are discussed throughout this document. This + document updates the SSH protocol as defined in [RFC4251] and related + documents. The security considerations of [RFC4251] apply. + + + +Bider Standards Track [Page 12] + +RFC 8308 Extension Negotiation in SSH March 2018 + + +6. References + +6.1. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH) + Protocol Assigned Numbers", RFC 4250, + DOI 10.17487/RFC4250, January 2006, + . + + [RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Protocol Architecture", RFC 4251, DOI 10.17487/RFC4251, + January 2006, . + + [RFC4252] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Authentication Protocol", RFC 4252, DOI 10.17487/RFC4252, + January 2006, . + + [RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253, + January 2006, . + + [RFC4254] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Connection Protocol", RFC 4254, DOI 10.17487/RFC4254, + January 2006, . + + [RFC8126] Cotton, M., Leiba, B., and T. Narten, "Guidelines for + Writing an IANA Considerations Section in RFCs", BCP 26, + RFC 8126, DOI 10.17487/RFC8126, June 2017, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + +6.2. Informative References + + [IANA-KE] IANA, "Key Exchange Method Names", + . + + [IANA-M] IANA, "Message Numbers", + . + + + + + +Bider Standards Track [Page 13] + +RFC 8308 Extension Negotiation in SSH March 2018 + + + [RFC8332] Bider, D., "Use of RSA Keys with SHA-256 and SHA-512 in + the Secure Shell (SSH) Protocol", RFC 8332, + DOI 10.17487/RFC8332, March 2018, + . + + [WINADMIN] Microsoft, "How to launch a process as a Full + Administrator when UAC is enabled?", March 2013, + . + + [WINTOKEN] Microsoft, "TOKEN_ELEVATION_TYPE enumeration", + . + +Acknowledgments + + Thanks to Markus Friedl and Damien Miller for comments and initial + implementation. Thanks to Peter Gutmann, Roumen Petrov, Mark D. + Baushke, Daniel Migault, Eric Rescorla, Matthew A. Miller, Mirja + Kuehlewind, Adam Roach, Spencer Dawkins, Alexey Melnikov, and Ben + Campbell for reviews and feedback. + +Author's Address + + Denis Bider + Bitvise Limited + 4105 Lombardy Court + Colleyville, TX 76034 + United States of America + + Email: ietf-ssh3@denisbider.com + URI: https://www.bitvise.com/ + + + + + + + + + + + + + + + + + + +Bider Standards Track [Page 14] + diff --git a/specifications/rfc8332.txt b/specifications/rfc8332.txt new file mode 100644 index 0000000..ff9ad57 --- /dev/null +++ b/specifications/rfc8332.txt @@ -0,0 +1,507 @@ + + + + + + +Internet Engineering Task Force (IETF) D. Bider +Request for Comments: 8332 Bitvise Limited +Updates: 4252, 4253 March 2018 +Category: Standards Track +ISSN: 2070-1721 + + + Use of RSA Keys with SHA-256 and SHA-512 + in the Secure Shell (SSH) Protocol + +Abstract + + This memo updates RFCs 4252 and 4253 to define new public key + algorithms for use of RSA keys with SHA-256 and SHA-512 for server + and client authentication in SSH connections. + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 7841. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + https://www.rfc-editor.org/info/rfc8332. + + + + + + + + + + + + + + + + + + + + + + +Bider Standards Track [Page 1] + +RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018 + + +Copyright Notice + + Copyright (c) 2018 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (https://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + + This document may contain material from IETF Documents or IETF + Contributions published or made publicly available before November + 10, 2008. The person(s) controlling the copyright in some of this + material may not have granted the IETF Trust the right to allow + modifications of such material outside the IETF Standards Process. + Without obtaining an adequate license from the person(s) controlling + the copyright in such materials, this document may not be modified + outside the IETF Standards Process, and derivative works of it may + not be created outside the IETF Standards Process, except to format + it for publication as an RFC or to translate it into languages other + than English. + +Table of Contents + + 1. Overview and Rationale . . . . . . . . . . . . . . . . . . . 3 + 1.1. Requirements Terminology . . . . . . . . . . . . . . . . 3 + 1.2. Wire Encoding Terminology . . . . . . . . . . . . . . . . 3 + 2. Public Key Format vs. Public Key Algorithm . . . . . . . . . 3 + 3. New RSA Public Key Algorithms . . . . . . . . . . . . . . . . 4 + 3.1. Use for Server Authentication . . . . . . . . . . . . . . 5 + 3.2. Use for Client Authentication . . . . . . . . . . . . . . 5 + 3.3. Discovery of Public Key Algorithms Supported by Servers . 6 + 4. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 6 + 5. Security Considerations . . . . . . . . . . . . . . . . . . . 7 + 5.1. Key Size and Signature Hash . . . . . . . . . . . . . . . 7 + 5.2. Transition . . . . . . . . . . . . . . . . . . . . . . . 7 + 5.3. PKCS #1 v1.5 Padding and Signature Verification . . . . . 7 + 6. References . . . . . . . . . . . . . . . . . . . . . . . . . 8 + 6.1. Normative References . . . . . . . . . . . . . . . . . . 8 + 6.2. Informative References . . . . . . . . . . . . . . . . . 8 + Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . 9 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 9 + + + + +Bider Standards Track [Page 2] + +RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018 + + +1. Overview and Rationale + + Secure Shell (SSH) is a common protocol for secure communication on + the Internet. In [RFC4253], SSH originally defined the public key + algorithms "ssh-rsa" for server and client authentication using RSA + with SHA-1, and "ssh-dss" using 1024-bit DSA and SHA-1. These + algorithms are now considered deficient. For US government use, NIST + has disallowed 1024-bit RSA and DSA, and use of SHA-1 for signing + [NIST.800-131A]. + + This memo updates RFCs 4252 and 4253 to define new public key + algorithms allowing for interoperable use of existing and new RSA + keys with SHA-256 and SHA-512. + +1.1. Requirements Terminology + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + +1.2. Wire Encoding Terminology + + The wire encoding types in this document -- "boolean", "byte", + "string", "mpint" -- have meanings as described in [RFC4251]. + +2. Public Key Format vs. Public Key Algorithm + + In [RFC4252], the concept "public key algorithm" is used to establish + a relationship between one algorithm name, and: + + A. procedures used to generate and validate a private/public + keypair; + B. a format used to encode a public key; and + C. procedures used to calculate, encode, and verify a signature. + + This document uses the term "public key format" to identify only A + and B in isolation. The term "public key algorithm" continues to + identify all three aspects -- A, B, and C. + + + + + + + + + + + +Bider Standards Track [Page 3] + +RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018 + + +3. New RSA Public Key Algorithms + + This memo adopts the style and conventions of [RFC4253] in specifying + how use of a public key algorithm is indicated in SSH. + + The following new public key algorithms are defined: + + rsa-sha2-256 RECOMMENDED sign Raw RSA key + rsa-sha2-512 OPTIONAL sign Raw RSA key + + These algorithms are suitable for use both in the SSH transport layer + [RFC4253] for server authentication and in the authentication layer + [RFC4252] for client authentication. + + Since RSA keys are not dependent on the choice of hash function, the + new public key algorithms reuse the "ssh-rsa" public key format as + defined in [RFC4253]: + + string "ssh-rsa" + mpint e + mpint n + + All aspects of the "ssh-rsa" format are kept, including the encoded + string "ssh-rsa". This allows existing RSA keys to be used with the + new public key algorithms, without requiring re-encoding or affecting + already trusted key fingerprints. + + Signing and verifying using these algorithms is performed according + to the RSASSA-PKCS1-v1_5 scheme in [RFC8017] using SHA-2 [SHS] as + hash. + + For the algorithm "rsa-sha2-256", the hash used is SHA-256. + For the algorithm "rsa-sha2-512", the hash used is SHA-512. + + The resulting signature is encoded as follows: + + string "rsa-sha2-256" / "rsa-sha2-512" + string rsa_signature_blob + + The value for 'rsa_signature_blob' is encoded as a string that + contains an octet string S (which is the output of RSASSA-PKCS1-v1_5) + and that has the same length (in octets) as the RSA modulus. When S + contains leading zeros, there exist signers that will send a shorter + encoding of S that omits them. A verifier MAY accept shorter + encodings of S with one or more leading zeros omitted. + + + + + + +Bider Standards Track [Page 4] + +RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018 + + +3.1. Use for Server Authentication + + To express support and preference for one or both of these algorithms + for server authentication, the SSH client or server includes one or + both algorithm names, "rsa-sha2-256" and/or "rsa-sha2-512", in the + name-list field "server_host_key_algorithms" in the SSH_MSG_KEXINIT + packet [RFC4253]. If one of the two host key algorithms is + negotiated, the server sends an "ssh-rsa" public key as part of the + negotiated key exchange method (e.g., in SSH_MSG_KEXDH_REPLY) and + encodes a signature with the appropriate signature algorithm name -- + either "rsa-sha2-256" or "rsa-sha2-512". + +3.2. Use for Client Authentication + + To use this algorithm for client authentication, the SSH client sends + an SSH_MSG_USERAUTH_REQUEST message [RFC4252] encoding the + "publickey" method and encoding the string field "public key + algorithm name" with the value "rsa-sha2-256" or "rsa-sha2-512". The + "public key blob" field encodes the RSA public key using the + "ssh-rsa" public key format. + + For example, as defined in [RFC4252] and [RFC4253], an SSH + "publickey" authentication request using an "rsa-sha2-512" signature + would be properly encoded as follows: + + byte SSH_MSG_USERAUTH_REQUEST + string user name + string service name + string "publickey" + boolean TRUE + string "rsa-sha2-512" + string public key blob: + string "ssh-rsa" + mpint e + mpint n + string signature: + string "rsa-sha2-512" + string rsa_signature_blob + + If the client includes the signature field, the client MUST encode + the same algorithm name in the signature as in + SSH_MSG_USERAUTH_REQUEST -- either "rsa-sha2-256" or "rsa-sha2-512". + If a server receives a mismatching request, it MAY apply arbitrary + authentication penalties, including but not limited to authentication + failure or disconnect. + + + + + + +Bider Standards Track [Page 5] + +RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018 + + + OpenSSH 7.2 (but not 7.2p2) incorrectly encodes the algorithm in the + signature as "ssh-rsa" when the algorithm in SSH_MSG_USERAUTH_REQUEST + is "rsa-sha2-256" or "rsa-sha2-512". In this case, the signature + does actually use either SHA-256 or SHA-512. A server MAY, but is + not required to, accept this variant or another variant that + corresponds to a good-faith implementation and is considered safe to + accept. + +3.3. Discovery of Public Key Algorithms Supported by Servers + + Implementation experience has shown that there are servers that apply + authentication penalties to clients attempting public key algorithms + that the SSH server does not support. + + Servers that accept rsa-sha2-* signatures for client authentication + SHOULD implement the extension negotiation mechanism defined in + [RFC8308], including especially the "server-sig-algs" extension. + + When authenticating with an RSA key against a server that does not + implement the "server-sig-algs" extension, clients MAY default to an + "ssh-rsa" signature to avoid authentication penalties. When the new + rsa-sha2-* algorithms have been sufficiently widely adopted to + warrant disabling "ssh-rsa", clients MAY default to one of the new + algorithms. + +4. IANA Considerations + + IANA has updated the "Secure Shell (SSH) Protocol Parameters" + registry, established with [RFC4250], to extend the table "Public Key + Algorithm Names" [IANA-PKA] as follows. + + - To the immediate right of the column "Public Key Algorithm Name", + a new column has been added, titled "Public Key Format". For + existing entries, the column "Public Key Format" has been assigned + the same value as under "Public Key Algorithm Name". + + - Immediately following the existing entry for "ssh-rsa", two + sibling entries have been added: + + P. K. Alg. Name P. K. Format Reference Note + rsa-sha2-256 ssh-rsa RFC 8332 Section 3 + rsa-sha2-512 ssh-rsa RFC 8332 Section 3 + + + + + + + + + +Bider Standards Track [Page 6] + +RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018 + + +5. Security Considerations + + The security considerations of [RFC4251] apply to this document. + +5.1. Key Size and Signature Hash + + The National Institute of Standards and Technology (NIST) Special + Publication 800-131A, Revision 1 [NIST.800-131A] disallows RSA and + DSA keys shorter than 2048 bits for US government use. The same + document disallows the SHA-1 hash function for digital signature + generation, except under NIST's protocol-specific guidance. + + It is prudent to follow this advice also outside of US government + use. + +5.2. Transition + + This document is based on the premise that RSA is used in + environments where a gradual, compatible transition to improved + algorithms will be better received than one that is abrupt and + incompatible. It advises that SSH implementations add support for + new RSA public key algorithms along with SSH_MSG_EXT_INFO and the + "server-sig-algs" extension to allow coexistence of new deployments + with older versions that support only "ssh-rsa". Nevertheless, + implementations SHOULD start to disable "ssh-rsa" in their default + configurations as soon as the implementers believe that new RSA + signature algorithms have been widely adopted. + +5.3. PKCS #1 v1.5 Padding and Signature Verification + + This document prescribes RSASSA-PKCS1-v1_5 signature padding because: + + (1) RSASSA-PSS is not universally available to all implementations; + (2) PKCS #1 v1.5 is widely supported in existing SSH + implementations; + (3) PKCS #1 v1.5 is not known to be insecure for use in this scheme. + + Implementers are advised that a signature with RSASSA-PKCS1-v1_5 + padding MUST NOT be verified by applying the RSA key to the + signature, and then parsing the output to extract the hash. This may + give an attacker opportunities to exploit flaws in the parsing and + vary the encoding. Verifiers MUST instead apply RSASSA-PKCS1-v1_5 + padding to the expected hash, then compare the encoded bytes with the + output of the RSA operation. + + + + + + + +Bider Standards Track [Page 7] + +RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018 + + +6. References + +6.1. Normative References + + [SHS] NIST, "Secure Hash Standard (SHS)", FIPS Publication + 180-4, August 2015, + . + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Protocol Architecture", RFC 4251, DOI 10.17487/RFC4251, + January 2006, . + + [RFC4252] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Authentication Protocol", RFC 4252, DOI 10.17487/RFC4252, + January 2006, . + + [RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253, + January 2006, . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [RFC8308] Bider, D., "Extension Negotiation in the Secure Shell + (SSH) Protocol", RFC 8308, DOI 10.17487/RFC8308, March + 2018, . + +6.2. Informative References + + [NIST.800-131A] + NIST, "Transitions: Recommendation for Transitioning the + Use of Cryptographic Algorithms and Key Lengths", NIST + Special Publication 800-131A, Revision 1, + DOI 10.6028/NIST.SP.800-131Ar1, November 2015, + . + + [RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH) + Protocol Assigned Numbers", RFC 4250, + DOI 10.17487/RFC4250, January 2006, + . + + + + +Bider Standards Track [Page 8] + +RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018 + + + [RFC8017] Moriarty, K., Ed., Kaliski, B., Jonsson, J., and A. Rusch, + "PKCS #1: RSA Cryptography Specifications Version 2.2", + RFC 8017, DOI 10.17487/RFC8017, November 2016, + . + + [IANA-PKA] + IANA, "Secure Shell (SSH) Protocol Parameters", + . + +Acknowledgments + + Thanks to Jon Bright, Niels Moeller, Stephen Farrell, Mark D. + Baushke, Jeffrey Hutzelman, Hanno Boeck, Peter Gutmann, Damien + Miller, Mat Berchtold, Roumen Petrov, Daniel Migault, Eric Rescorla, + Russ Housley, Alissa Cooper, Adam Roach, and Ben Campbell for + reviews, comments, and suggestions. + +Author's Address + + Denis Bider + Bitvise Limited + 4105 Lombardy Court + Colleyville, Texas 76034 + United States of America + + Email: ietf-ssh3@denisbider.com + URI: https://www.bitvise.com/ + + + + + + + + + + + + + + + + + + + + + + + + +Bider Standards Track [Page 9] + diff --git a/specifications/rfc8709.txt b/specifications/rfc8709.txt new file mode 100644 index 0000000..b50e8aa --- /dev/null +++ b/specifications/rfc8709.txt @@ -0,0 +1,317 @@ + + + + +Internet Engineering Task Force (IETF) B. Harris +Request for Comments: 8709 +Updates: 4253 L. Velvindron +Category: Standards Track cyberstorm.mu +ISSN: 2070-1721 February 2020 + + + Ed25519 and Ed448 Public Key Algorithms for the Secure Shell (SSH) + Protocol + +Abstract + + This document describes the use of the Ed25519 and Ed448 digital + signature algorithms in the Secure Shell (SSH) protocol. + Accordingly, this RFC updates RFC 4253. + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 7841. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + https://www.rfc-editor.org/info/rfc8709. + +Copyright Notice + + Copyright (c) 2020 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (https://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + +Table of Contents + + 1. Introduction + 2. Conventions Used in This Document + 2.1. Requirements Language + 3. Public Key Algorithm + 4. Public Key Format + 5. Signature Algorithm + 6. Signature Format + 7. Verification Algorithm + 8. SSHFP DNS Resource Records + 9. IANA Considerations + 10. Security Considerations + 11. References + 11.1. Normative References + 11.2. Informative References + Acknowledgements + Authors' Addresses + +1. Introduction + + Secure Shell (SSH) [RFC4251] is a secure remote-login protocol. It + provides for an extensible variety of public key algorithms for + identifying servers and users to one another. Ed25519 [RFC8032] is a + digital signature system. OpenSSH 6.5 [OpenSSH-6.5] introduced + support for using Ed25519 for server and user authentication and was + then followed by other SSH implementations. + + This document describes the method implemented by OpenSSH and others + and formalizes the use of the name "ssh-ed25519". Additionally, this + document describes the use of Ed448 and formalizes the use of the + name "ssh-ed448". + +2. Conventions Used in This Document + + The descriptions of key and signature formats use the notation + introduced in [RFC4251], Section 3 and the string data type from + [RFC4251], Section 5. + +2.1. Requirements Language + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + +3. Public Key Algorithm + + This document describes a public key algorithm for use with SSH, as + per [RFC4253], Section 6.6. The name of the algorithm is "ssh- + ed25519". This algorithm only supports signing and not encryption. + + Additionally, this document describes another public key algorithm. + The name of the algorithm is "ssh-ed448". This algorithm only + supports signing and not encryption. + + Standard implementations of SSH SHOULD implement these signature + algorithms. + +4. Public Key Format + + The "ssh-ed25519" key format has the following encoding: + + string "ssh-ed25519" + + string key + + Here, 'key' is the 32-octet public key described in [RFC8032], + Section 5.1.5. + + The "ssh-ed448" key format has the following encoding: + + string "ssh-ed448" + + string key + + Here, 'key' is the 57-octet public key described in [RFC8032], + Section 5.2.5. + +5. Signature Algorithm + + Signatures are generated according to the procedure in Sections 5.1.6 + and 5.2.6 of [RFC8032]. + +6. Signature Format + + The "ssh-ed25519" key format has the following encoding: + + string "ssh-ed25519" + + string signature + + Here, 'signature' is the 64-octet signature produced in accordance + with [RFC8032], Section 5.1.6. + + The "ssh-ed448" key format has the following encoding: + + string "ssh-ed448" + + string signature + + Here, 'signature' is the 114-octet signature produced in accordance + with [RFC8032], Section 5.2.6. + +7. Verification Algorithm + + Ed25519 signatures are verified according to the procedure in + [RFC8032], Section 5.1.7. + + Ed448 signatures are verified according to the procedure in + [RFC8032], Section 5.2.7. + +8. SSHFP DNS Resource Records + + Usage and generation of the SSHFP DNS resource record is described in + [RFC4255]. The generation of SSHFP resource records for "ssh- + ed25519" keys is described in [RFC7479]. This section illustrates + the generation of SSHFP resource records for "ssh-ed448" keys, and + this document also specifies the corresponding Ed448 code point to + "SSHFP RR Types for public key algorithms" in the "DNS SSHFP Resource + Record Parameters" IANA registry [IANA-SSHFP]. + + The generation of SSHFP resource records for "ssh-ed448" keys is + described as follows. + + The encoding of Ed448 public keys is described in [ED448]. In brief, + an Ed448 public key is a 57-octet value representing a 455-bit + y-coordinate of an elliptic curve point, and a sign bit indicating + the corresponding x-coordinate. + + The SSHFP Resource Record for the Ed448 public key with SHA-256 + fingerprint would, for example, be: + + example.com. IN SSHFP 6 2 ( a87f1b687ac0e57d2a081a2f2826723 + 34d90ed316d2b818ca9580ea384d924 + 01 ) + + The '2' here indicates SHA-256 [RFC6594]. + +9. IANA Considerations + + This document augments the Public Key Algorithm Names in [RFC4250], + Section 4.11.3. + + IANA has added the following entry to "Public Key Algorithm Names" in + the "Secure Shell (SSH) Protocol Parameters" registry [IANA-SSH]: + + +---------------------------+-----------+ + | Public Key Algorithm Name | Reference | + +===========================+===========+ + | ssh-ed25519 | RFC 8709 | + +---------------------------+-----------+ + | ssh-ed448 | RFC 8709 | + +---------------------------+-----------+ + + Table 1 + + IANA has added the following entry to "SSHFP RR Types for public key + algorithms" in the "DNS SSHFP Resource Record Parameters" registry + [IANA-SSHFP]: + + +-------+-------------+-----------+ + | Value | Description | Reference | + +=======+=============+===========+ + | 6 | Ed448 | RFC 8709 | + +-------+-------------+-----------+ + + Table 2 + +10. Security Considerations + + The security considerations in [RFC4251], Section 9 apply to all SSH + implementations, including those using Ed25519 and Ed448. + + The security considerations in [RFC8032], Section 8 and [RFC7479], + Section 3 apply to all uses of Ed25519 and Ed448, including those in + SSH. + +11. References + +11.1. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH) + Protocol Assigned Numbers", RFC 4250, + DOI 10.17487/RFC4250, January 2006, + . + + [RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Protocol Architecture", RFC 4251, DOI 10.17487/RFC4251, + January 2006, . + + [RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253, + January 2006, . + + [RFC4255] Schlyter, J. and W. Griffin, "Using DNS to Securely + Publish Secure Shell (SSH) Key Fingerprints", RFC 4255, + DOI 10.17487/RFC4255, January 2006, + . + + [RFC6594] Sury, O., "Use of the SHA-256 Algorithm with RSA, Digital + Signature Algorithm (DSA), and Elliptic Curve DSA (ECDSA) + in SSHFP Resource Records", RFC 6594, + DOI 10.17487/RFC6594, April 2012, + . + + [RFC8032] Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital + Signature Algorithm (EdDSA)", RFC 8032, + DOI 10.17487/RFC8032, January 2017, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + +11.2. Informative References + + [ED448] Hamburg, M., "Ed448-Goldilocks, a new elliptic curve", + January 2015, . + + [IANA-SSH] IANA, "Secure Shell (SSH) Protocol Parameters", + . + + [IANA-SSHFP] + IANA, "DNS SSHFP Resource Record Parameters", + . + + [OpenSSH-6.5] + Friedl, M., Provos, N., de Raadt, T., Steves, K., Miller, + D., Tucker, D., McIntyre, J., Rice, T., and B. Lindstrom, + "OpenSSH 6.5 release notes", January 2014, + . + + [RFC7479] Moonesamy, S., "Using Ed25519 in SSHFP Resource Records", + RFC 7479, DOI 10.17487/RFC7479, March 2015, + . + +Acknowledgements + + The OpenSSH implementation of Ed25519 in SSH was written by Markus + Friedl. We are also grateful to Mark Baushke, Benjamin Kaduk, and + Daniel Migault for their comments. + +Authors' Addresses + + Ben Harris + 2A Eachard Road + Cambridge + CB3 0HY + United Kingdom + + Email: bjh21@bjh21.me.uk + + + Loganaden Velvindron + cyberstorm.mu + 88, Avenue De Plevitz + Roches Brunes + Mauritius + + Email: logan@cyberstorm.mu diff --git a/specifications/rfc8758.txt b/specifications/rfc8758.txt new file mode 100644 index 0000000..312280c --- /dev/null +++ b/specifications/rfc8758.txt @@ -0,0 +1,196 @@ + + + + +Internet Engineering Task Force (IETF) L. Velvindron +Request for Comments: 8758 cyberstorm.mu +BCP: 227 April 2020 +Updates: 4253 +Category: Best Current Practice +ISSN: 2070-1721 + + + Deprecating RC4 in Secure Shell (SSH) + +Abstract + + This document deprecates RC4 in Secure Shell (SSH). Therefore, this + document formally moves RFC 4345 to Historic status. + +Status of This Memo + + This memo documents an Internet Best Current Practice. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + BCPs is available in Section 2 of RFC 7841. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + https://www.rfc-editor.org/info/rfc8758. + +Copyright Notice + + Copyright (c) 2020 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (https://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + +Table of Contents + + 1. Introduction + 1.1. Requirements Language + 2. Updates to RFC 4253 + 3. IANA Considerations + 4. Security Considerations + 5. References + 5.1. Normative References + 5.2. Informative References + Acknowledgements + Author's Address + +1. Introduction + + The usage of RC4 suites (also designated as "arcfour") for SSH is + specified in [RFC4253] and [RFC4345]. [RFC4253] specifies the + allocation of the "arcfour" cipher for SSH. [RFC4345] specifies and + allocates the "arcfour128" and "arcfour256" ciphers for SSH. RC4 + encryption has known weaknesses [RFC7465] [RFC8429]; therefore, this + document starts the deprecation process for their use in Secure Shell + (SSH) [RFC4253]. Accordingly, [RFC4253] is updated to note the + deprecation of the RC4 ciphers, and [RFC4345] is moved to Historic + status, as all ciphers it specifies MUST NOT be used. + +1.1. Requirements Language + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + +2. Updates to RFC 4253 + + [RFC4253] is updated to prohibit arcfour's use in SSH. [RFC4253], + Section 6.3 allocates the "arcfour" cipher by defining a list of + defined ciphers in which the "arcfour" cipher appears as optional, as + shown in Table 1. + + +---------+----------+----------------------------------------------+ + | arcfour | OPTIONAL | the ARCFOUR stream cipher | + | | | with a 128-bit key | + +---------+----------+----------------------------------------------+ + + Table 1 + + This document updates the status of the "arcfour" ciphers in the list + found in [RFC4253], Section 6.3 by moving it from OPTIONAL to MUST + NOT. + + +---------+----------+----------------------------------------------+ + | arcfour | MUST NOT | the ARCFOUR stream cipher | + | | | with a 128-bit key | + +---------+----------+----------------------------------------------+ + + Table 2 + + [RFC4253] defines the "arcfour" ciphers with the following text: + + | The "arcfour" cipher is the Arcfour stream cipher with 128-bit + | keys. The Arcfour cipher is believed to be compatible with the + | RC4 cipher [SCHNEIER]. Arcfour (and RC4) has problems with weak + | keys, and should be used with caution. + + This document updates [RFC4253], Section 6.3 by replacing the text + above with the following text: + + | The "arcfour" cipher is the Arcfour stream cipher with 128-bit + | keys. The Arcfour cipher is compatible with the RC4 cipher + | [SCHNEIER]. Arcfour (and RC4) has known weaknesses [RFC7465] + | [RFC8429] and MUST NOT be used. + +3. IANA Considerations + + The IANA has updated the "Encryption Algorithm Names" subregistry in + the "Secure Shell (SSH) Protocol Parameters" registry [IANA]. The + registration procedure is IETF review, which is achieved by this + document. The registry has been updated as follows: + + +---------------------------+-----------+----------+ + | Encryption Algorithm Name | Reference | Note | + +===========================+===========+==========+ + | arcfour | RFC 8758 | HISTORIC | + +---------------------------+-----------+----------+ + | arcfour128 | RFC 8758 | HISTORIC | + +---------------------------+-----------+----------+ + | arcfour256 | RFC 8758 | HISTORIC | + +---------------------------+-----------+----------+ + + Table 3 + +4. Security Considerations + + This document only prohibits the use of RC4 in SSH; it introduces no + new security considerations. + +5. References + +5.1. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + +5.2. Informative References + + [IANA] "Secure Shell (SSH) Protocol Parameters", + . + + [RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253, + January 2006, . + + [RFC4345] Harris, B., "Improved Arcfour Modes for the Secure Shell + (SSH) Transport Layer Protocol", RFC 4345, + DOI 10.17487/RFC4345, January 2006, + . + + [RFC7465] Popov, A., "Prohibiting RC4 Cipher Suites", RFC 7465, + DOI 10.17487/RFC7465, February 2015, + . + + [RFC8429] Kaduk, B. and M. Short, "Deprecate Triple-DES (3DES) and + RC4 in Kerberos", BCP 218, RFC 8429, DOI 10.17487/RFC8429, + October 2018, . + + [SCHNEIER] Schneier, B., "Applied Cryptography Second Edition: + Protocols, Algorithms, and Source in Code in C", John + Wiley and Sons New York, NY, 1996. + +Acknowledgements + + The author would like to thank Eric Rescorla, Daniel Migault, and + Rich Salz. + +Author's Address + + Loganaden Velvindron + cyberstorm.mu + Mauritius + + Email: logan@cyberstorm.mu diff --git a/specifications/rfc9142.txt b/specifications/rfc9142.txt new file mode 100644 index 0000000..470fcd2 --- /dev/null +++ b/specifications/rfc9142.txt @@ -0,0 +1,1028 @@ + + + + +Internet Engineering Task Force (IETF) M. Baushke +Request for Comments: 9142 January 2022 +Updates: 4250, 4253, 4432, 4462 +Category: Standards Track +ISSN: 2070-1721 + + + Key Exchange (KEX) Method Updates and Recommendations for Secure Shell + (SSH) + +Abstract + + This document updates the recommended set of key exchange methods for + use in the Secure Shell (SSH) protocol to meet evolving needs for + stronger security. It updates RFCs 4250, 4253, 4432, and 4462. + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 7841. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + https://www.rfc-editor.org/info/rfc9142. + +Copyright Notice + + Copyright (c) 2022 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (https://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Revised BSD License text as described in Section 4.e of the + Trust Legal Provisions and are provided without warranty as described + in the Revised BSD License. + +Table of Contents + + 1. Overview and Rationale + 1.1. Selecting an Appropriate Hashing Algorithm + 1.2. Selecting an Appropriate Public Key Algorithm + 1.2.1. Elliptic Curve Cryptography (ECC) + 1.2.2. Finite Field Cryptography (FFC) + 1.2.3. Integer Factorization Cryptography (IFC) + 2. Requirements Language + 3. Key Exchange Methods + 3.1. Elliptic Curve Cryptography (ECC) + 3.1.1. curve25519-sha256 and gss-curve25519-sha256-* + 3.1.2. curve448-sha512 and gss-curve448-sha512-* + 3.1.3. ecdh-*, ecmqv-sha2, and gss-nistp* + 3.2. Finite Field Cryptography (FFC) + 3.2.1. FFC Diffie-Hellman Using Generated MODP Groups + 3.2.2. FFC Diffie-Hellman Using Named MODP Groups + 3.3. Integer Factorization Cryptography (IFC) + 3.4. KDFs and Integrity Hashing + 3.5. Secure Shell Extension Negotiation + 4. Summary Guidance for Implementation of Key Exchange Method + Names + 5. Security Considerations + 6. IANA Considerations + 7. References + 7.1. Normative References + 7.2. Informative References + Acknowledgements + Author's Address + +1. Overview and Rationale + + Secure Shell (SSH) is a common protocol for secure communication on + the Internet. In [RFC4253], SSH originally defined two Key Exchange + (KEX) Method Names that MUST be implemented. Over time, what was + once considered secure is no longer considered secure. The purpose + of this RFC is to recommend that some published key exchanges be + deprecated or disallowed as well as to recommend some that SHOULD and + one that MUST be adopted. + + This document updates [RFC4250], [RFC4253], [RFC4432], and [RFC4462] + by changing the requirement level ("MUST" moving to "SHOULD", "MAY", + or "SHOULD NOT", and "MAY" moving to "MUST", "SHOULD", "SHOULD NOT", + or "MUST NOT") of various key exchange mechanisms. Some + recommendations will be unchanged but are included for completeness. + + Section 7.2 of [RFC4253] says the following: + + | The key exchange produces two values: a shared secret K, and an + | exchange hash H. Encryption and authentication keys are derived + | from these. The exchange hash H from the first key exchange is + | additionally used as the session identifier, which is a unique + | identifier for this connection. It is used by authentication + | methods as a part of the data that is signed as a proof of + | possession of a private key. Once computed, the session + | identifier is not changed, even if keys are later re-exchanged. + + The security strength of the public key exchange algorithm and the + hash used in the Key Derivation Function (KDF) both impact the + security of the shared secret K being used. + + The hashing algorithms used by key exchange methods described in this + document are: sha1, sha256, sha384, and sha512. In many cases, the + hash name is explicitly appended to the public key exchange algorithm + name. However, some of them are implicit and defined in the RFC that + defines the key exchange algorithm name. + + Various RFCs use different spellings and capitalizations for the + hashing function and encryption function names. For the purpose of + this document, the following are equivalent names: sha1, SHA1, and + SHA-1; sha256, SHA256, SHA-256, and SHA2-256; sha384, SHA384, SHA- + 384, and SHA2-384; and sha512, SHA512, SHA-512, and SHA2-512. + + For the purpose of this document, the following are equivalent: + aes128, AES128, AES-128; aes192, AES192, and AES-192; and aes256, + AES256, and AES-256. + + It is good to try to match the security strength of the public key + exchange algorithm with the security strength of the symmetric + cipher. + + There are many possible symmetric ciphers available with multiple + modes. The list in Table 1 is intended as a representative sample of + those that appear to be present in most SSH implementations. The + security strength estimates are generally available in [RFC4086] for + triple-DES and AES, as well as Section 5.6.1.1 of + [NIST.SP.800-57pt1r5]. + + +========================+=============================+ + | Cipher Name (modes) | Estimated Security Strength | + +========================+=============================+ + | 3des (cbc) | 112 bits | + +------------------------+-----------------------------+ + | aes128 (cbc, ctr, gcm) | 128 bits | + +------------------------+-----------------------------+ + | aes192 (cbc, ctr, gcm) | 192 bits | + +------------------------+-----------------------------+ + | aes256 (cbc, ctr, gcm) | 256 bits | + +------------------------+-----------------------------+ + + Table 1: Symmetric Cipher Security Strengths + + The following subsections describe how to select each component of + the key exchange. + +1.1. Selecting an Appropriate Hashing Algorithm + + The SHA-1 hash is in the process of being deprecated for many + reasons. + + There have been attacks against SHA-1, and it is no longer strong + enough for SSH security requirements. Therefore, it is desirable to + move away from using it before attacks become more serious. + + The SHA-1 hash provides for approximately 80 bits of security + strength. This means that the shared key being used has at most 80 + bits of security strength, which may not be sufficient for most + users. + + For purposes of key exchange methods, attacks against SHA-1 are + collision attacks that usually rely on human help rather than a pre- + image attack. The SHA-1 hash resistance against a second pre-image + is still at 160 bits, but SSH does not depend on second pre-image + resistance but rather on chosen-prefix collision resistance. + + Transcript Collision attacks are documented in [TRANSCRIPTION]. This + paper shows that an on-path attacker does not tamper with the Diffie- + Hellman values and does not know the connection keys. The attack + could be used to tamper with both I_C and I_S (as defined in + Section 7.3 of [RFC4253]) and might potentially be able to downgrade + the negotiated ciphersuite to a weak cryptographic algorithm that the + attacker knows how to break. + + These attacks are still computationally very difficult to perform, + but it is desirable that any key exchange using SHA-1 be phased out + as soon as possible. + + If there is a need for using SHA-1 in a key exchange for + compatibility, it would be desirable to list it last in the + preference list of key exchanges. + + Use of the SHA-2 family of hashes found in [RFC6234] rather than the + SHA-1 hash is strongly advised. + + When it comes to the SHA-2 family of secure hashing functions, + SHA2-256 has 128 bits of security strength; SHA2-384 has 192 bits of + security strength; and SHA2-512 has 256 bits of security strength. + It is suggested that the minimum secure hashing function used for key + exchange methods should be SHA2-256 with 128 bits of security + strength. Other hashing functions may also have the same number of + bits of security strength, but none are as yet defined in any RFC for + use in a KEX for SSH. + + To avoid combinatorial explosion of key exchange names, newer key + exchanges are generally restricted to *-sha256 and *-sha512. The + exceptions are ecdh-sha2-nistp384 and gss-nistp384-sha384-*, which + are defined to use SHA2-384 (also known as SHA-384) defined in + [RFC6234] for the hash algorithm. + + Table 2 provides a summary of security strength for hashing functions + for collision resistance. You may consult [NIST.SP.800-107r1] for + more information on hash algorithm security strength. + + +===========+=============================+ + | Hash Name | Estimated Security Strength | + +===========+=============================+ + | sha1 | 80 bits (before attacks) | + +-----------+-----------------------------+ + | sha256 | 128 bits | + +-----------+-----------------------------+ + | sha384 | 192 bits | + +-----------+-----------------------------+ + | sha512 | 256 bits | + +-----------+-----------------------------+ + + Table 2: Hashing Function Security + Strengths + +1.2. Selecting an Appropriate Public Key Algorithm + + SSH uses mathematically hard problems for doing key exchanges: + + * Elliptic Curve Cryptography (ECC) has families of curves for key + exchange methods for SSH. NIST prime curves with names and other + curves are available using an object identifier (OID) with + Elliptic Curve Diffie-Hellman (ECDH) via [RFC5656]. Curve25519 + and curve448 key exchanges are used with ECDH via [RFC8731]. + + * Finite Field Cryptography (FFC) is used for Diffie-Hellman (DH) + key exchange with "safe primes" either from a specified list found + in [RFC3526] or generated dynamically via [RFC4419] as updated by + [RFC8270]. + + * Integer Factorization Cryptography (IFC) using the RSA algorithm + is provided for in [RFC4432]. + + It is desirable that the security strength of the key exchange be + chosen to be comparable with the security strength of the other + elements of the SSH handshake. Attackers can target the weakest + element of the SSH handshake. + + It is desirable that a minimum of 112 bits of security strength be + selected to match the weakest of the symmetric cipher (3des-cbc) + available. Based on implementer security needs, a stronger minimum + may be desired. + + The larger the Modular Exponentiation (MODP) group, the ECC curve + size, or the RSA key length, the more computation power will be + required to perform the key exchange. + +1.2.1. Elliptic Curve Cryptography (ECC) + + For ECC, across all of the named curves, the minimum security + strength is approximately 128 bits. The [RFC5656] key exchanges for + the named curves use a hashing function with a matching security + strength. Likewise, the [RFC8731] key exchanges use a hashing + function that has more security strength than the curves. The + minimum strength will be the security strength of the curve. Table 3 + contains a breakdown of just the ECC security strength by curve name; + it does include the hashing algorithm used. The curve25519 and + curve488 security-level numbers are in [RFC7748]. The nistp256, + nistp384, and nistp521 (NIST prime curves) are provided in [RFC5656]. + The hashing algorithm designated for use with the individual curves + have approximately the same number of bits of security as the named + curve. + + +============+=============================+ + | Curve Name | Estimated Security Strength | + +============+=============================+ + | nistp256 | 128 bits | + +------------+-----------------------------+ + | nistp384 | 192 bits | + +------------+-----------------------------+ + | nistp521 | 512 bits | + +------------+-----------------------------+ + | curve25519 | 128 bits | + +------------+-----------------------------+ + | curve448 | 224 bits | + +------------+-----------------------------+ + + Table 3: ECC Security Strengths + +1.2.2. Finite Field Cryptography (FFC) + + For FFC, it is recommended to use a modulus with a minimum of 2048 + bits (approximately 112 bits of security strength) with a hash that + has at least as many bits of security as the FFC. The security + strength of the FFC and the hash together will be the minimum of + those two values. This is sufficient to provide a consistent + security strength for the 3des-cbc cipher. Section 1 of [RFC3526] + notes that the Advanced Encryption Standard (AES) cipher, which has + more strength, needs stronger groups. For the 128-bit AES, we need + about a 3200-bit group. The 192- and 256-bit keys would need groups + that are about 8000 and 15400 bits, respectively. Table 4 provides + the security strength of the MODP group. When paired with a hashing + algorithm, the security strength will be the minimum of the two + algorithms. + + +==================+=============================+============+ + | Prime Field Size | Estimated Security Strength | Example | + | | | MODP Group | + +==================+=============================+============+ + | 2048-bit | 112 bits | group14 | + +------------------+-----------------------------+------------+ + | 3072-bit | 128 bits | group15 | + +------------------+-----------------------------+------------+ + | 4096-bit | 152 bits | group16 | + +------------------+-----------------------------+------------+ + | 6144-bit | 176 bits | group17 | + +------------------+-----------------------------+------------+ + | 8192-bit | 200 bits | group18 | + +------------------+-----------------------------+------------+ + + Table 4: FFC MODP Security Strengths + + The minimum MODP group is the 2048-bit MODP group14. When used with + a SHA-1 hash, this group provides approximately 80 bits of security. + When used with a SHA2-256 hash, this group provides approximately 112 + bits of security. The 3des-cbc cipher itself provides at most 112 + bits of security, so the group14-sha256 key exchanges is sufficient + to keep all of the 3des-cbc key, for 112 bits of security. + + A 3072-bit MODP group when used with a SHA2-256 hash will provide + approximately 128 bits of security. This is desirable when using a + cipher such as aes128 or chacha20-poly1305 that provides + approximately 128 bits of security. + + The 8192-bit group18 MODP group when used with sha512 provides + approximately 200 bits of security, which is sufficient to protect + aes192 with 192 bits of security. + +1.2.3. Integer Factorization Cryptography (IFC) + + The only IFC algorithm for key exchange is the RSA algorithm + specified in [RFC4432]. RSA 1024-bit keys have approximately 80 bits + of security strength. RSA 2048-bit keys have approximately 112 bits + of security strength. It is worth noting that the IFC types of key + exchange do not provide Forward Secrecy, which both FFC and ECC do + provide. + + In order to match the 112 bits of security strength needed for 3des- + cbc, an RSA 2048-bit key matches the security strength. The use of a + SHA-2 family hash with RSA 2048-bit keys has sufficient security to + match the 3des-cbc symmetric cipher. The rsa1024-sha1 key exchange + has approximately 80 bits of security strength and is not desirable. + + Table 5 summarizes the security strengths of these key exchanges + without including the hashing algorithm strength. Guidance for these + strengths can be found in Section 5.6.1.1 of [NIST.SP.800-57pt1r5]. + + +=====================+=============================+ + | Key Exchange Method | Estimated Security Strength | + +=====================+=============================+ + | rsa1024-sha1 | 80 bits | + +---------------------+-----------------------------+ + | rsa2048-sha256 | 112 bits | + +---------------------+-----------------------------+ + + Table 5: IFC Security Strengths + +2. Requirements Language + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + +3. Key Exchange Methods + + This document adopts the style and conventions of [RFC4253] in + specifying how the use of data key exchange is indicated in SSH. + + This RFC also collects key exchange method names in various existing + RFCs ([RFC4253], [RFC4419], [RFC4432], [RFC4462], [RFC5656], + [RFC8268], [RFC8308], [RFC8731], and [RFC8732]) and provides a + suggested suitability for implementation of MUST, SHOULD, MAY, SHOULD + NOT, and MUST NOT. Any method not explicitly listed MAY be + implemented. + + Section 7.2 of [RFC4253] defines the generation of a shared secret K + (really the output of the KDF) and an exchange key hash H. Each key + exchange method uses a specified HASH function, which must be the + same for both key exchange and Key Derivation. H is used for key + exchange integrity across the SSH session as it is computed only + once. It is noted at the end of Section 7.2 of [RFC4253] that: + + | This process will lose entropy if the amount of entropy in K is + | larger than the internal state size of HASH. + + So, care must be taken that the hashing algorithm used is well chosen + ("reasonable") for the key exchange algorithms being used. + + This document provides guidance as to what key exchange algorithms + are to be considered for new or updated SSH implementations. + + In general, key exchange methods that are considered "weak" are being + moved to either deprecated ("SHOULD NOT") or disallowed ("MUST NOT"). + Methods that are newer or considered to be stronger usually require + more device resources than many administrators and/or developers need + are to be allowed ("MAY"). (Eventually, some of these methods could + be moved by consensus to "SHOULD" to increase interoperability and + security.) Methods that are not "weak" and have implementation + consensus are encouraged ("SHOULD"). There needs to be at least one + consensus method promoted to a status of mandatory to implement + (MTI). This should help to provide continued interoperability even + with the loss of one of the now disallowed MTI methods. + + For this document, 112 bits of security strength is the minimum. Use + of either or both of SHA-1 and RSA 1024 bits at an approximate 80 + bits of security fall below this minimum and should be deprecated and + moved to disallowed as quickly as possible in configured deployments + of SSH. It seems plausible that this minimum may be increased over + time, so authors and administrators may wish to prepare for a switch + to algorithms that provide more security strength. + +3.1. Elliptic Curve Cryptography (ECC) + + The Elliptic Curve (EC) key exchange algorithms used with SSH include + the ECDH and EC Menezes-Qu-Vanstone (ECMQV). + + The ECC curves defined for the key exchange algorithms above include + the following: curve25519, curve448, the NIST prime curves (nistp256, + nistp384, and nistp521), as well as other curves allowed for by + Section 6 of [RFC5656]. There are key exchange mechanisms based on + the Generic Security Service Application Program Interface (GSS-API) + that use these curves as well that have a "gss-" prefix. + +3.1.1. curve25519-sha256 and gss-curve25519-sha256-* + + Curve25519 is efficient on a wide range of architectures with + properties that allow higher-performance implementations compared to + the patented elliptic curve parameters purchased by NIST for the + general public to use as described in [RFC5656]. The corresponding + key exchange methods use SHA2-256 (also known as SHA-256) defined in + [RFC6234]. SHA2-256 is a reasonable hash for use in both the KDF and + session integrity. It is reasonable for both gss and non-gss uses of + curve25519 key exchange methods. These key exchange methods are + described in [RFC8731] and [RFC8732] and are similar to the IKEv2 key + agreement described in [RFC8031]. The curve25519-sha256 key exchange + method has multiple implementations and SHOULD be implemented. The + gss-curve25519-sha256-* key exchange method SHOULD also be + implemented because it shares the same performance and security + characteristics as curve25519-sha256. + + Table 6 contains a summary of the recommendations for + curve25519-based key exchanges. + + +==========================+==========+ + | Key Exchange Method Name | Guidance | + +==========================+==========+ + | curve25519-sha256 | SHOULD | + +--------------------------+----------+ + | gss-curve25519-sha256-* | SHOULD | + +--------------------------+----------+ + + Table 6: Curve25519 Implementation + Guidance + +3.1.2. curve448-sha512 and gss-curve448-sha512-* + + Curve448 provides more security strength than curve25519 at a higher + computational and bandwidth cost. The corresponding key exchange + methods use SHA2-512 (also known as SHA-512) defined in [RFC6234]. + SHA2-512 is a reasonable hash for use in both the KDF and session + integrity. It is reasonable for both gss and non-gss uses of + curve448 key exchange methods. These key exchange methods are + described in [RFC8731] and [RFC8732] and are similar to the IKEv2 key + agreement described in [RFC8031]. The curve448-sha512 key exchange + method MAY be implemented. The gss-curve448-sha512-* key exchange + method MAY also be implemented because it shares the same performance + and security characteristics as curve448-sha512. + + Table 7 contains a summary of the recommendations for curve448-based + key exchanges. + + +==========================+==========+ + | Key Exchange Method Name | Guidance | + +==========================+==========+ + | curve448-sha512 | MAY | + +--------------------------+----------+ + | gss-curve448-sha512-* | MAY | + +--------------------------+----------+ + + Table 7: Curve448 Implementation + Guidance + +3.1.3. ecdh-*, ecmqv-sha2, and gss-nistp* + + The ecdh-sha2-* namespace allows for both the named NIST prime curves + (nistp256, nistp384, and nistp521) as well as other curves to be + defined for the ECDH key exchange. At the time of this writing, + there are three named curves in this namespace that SHOULD be + supported. They appear in Section 10.1 of [RFC5656]. If + implemented, the named curves SHOULD always be enabled unless + specifically disabled by local security policy. In Section 6.1 of + [RFC5656], the method to name other ECDH curves using OIDs is + specified. These other curves MAY be implemented. + + The GSS-API namespace with gss-nistp*-sha* mirrors the algorithms + used by ecdh-sha2-* names. They are described in [RFC8732]. + + ECDH reduces bandwidth of key exchanges compared to FFC DH at a + similar security strength. + + Table 8 lists algorithms as "SHOULD" where implementations may be + more efficient or widely deployed. The items listed as "MAY" in + Table 8 are potentially less efficient. + + +==========================+==========+ + | Key Exchange Method Name | Guidance | + +==========================+==========+ + | ecdh-sha2-* | MAY | + +--------------------------+----------+ + | ecdh-sha2-nistp256 | SHOULD | + +--------------------------+----------+ + | gss-nistp256-sha256-* | SHOULD | + +--------------------------+----------+ + | ecdh-sha2-nistp384 | SHOULD | + +--------------------------+----------+ + | gss-nistp384-sha384-* | SHOULD | + +--------------------------+----------+ + | ecdh-sha2-nistp521 | SHOULD | + +--------------------------+----------+ + | gss-nistp521-sha512-* | SHOULD | + +--------------------------+----------+ + | ecmqv-sha2 | MAY | + +--------------------------+----------+ + + Table 8: ECDH Implementation Guidance + + It is advisable to match the Elliptic Curve Digital Signature + Algorithm (ECDSA) and ECDH algorithm to use the same curve for both + to maintain the same security strength in the connection. + +3.2. Finite Field Cryptography (FFC) + +3.2.1. FFC Diffie-Hellman Using Generated MODP Groups + + [RFC4419] defines two key exchange methods that use a random + selection from a set of pre-generated moduli for key exchange: the + diffie-hellman-group-exchange-sha1 method and the diffie-hellman- + group-exchange-sha256 method. Per [RFC8270], implementations SHOULD + use a MODP group whose modulus size is equal to or greater than 2048 + bits. MODP groups with a modulus size less than 2048 bits are weak + and MUST NOT be used. + + The diffie-hellman-group-exchange-sha1 key exchange method SHOULD NOT + be used. This method uses SHA-1, which is being deprecated. + + The diffie-hellman-group-exchange-sha256 key exchange method MAY be + used. This method uses SHA2-256, which is reasonable for MODP groups + less than 4096 bits. + + Care should be taken in the pre-generation of the moduli P and + generator G such that the generator provides a Q-ordered subgroup of + P. Otherwise, the parameter set may leak one bit of the shared + secret. + + Table 9 provides a summary of the guidance for these exchanges. + + +======================================+============+ + | Key Exchange Method Name | Guidance | + +======================================+============+ + | diffie-hellman-group-exchange-sha1 | SHOULD NOT | + +--------------------------------------+------------+ + | diffie-hellman-group-exchange-sha256 | MAY | + +--------------------------------------+------------+ + + Table 9: FFC Generated MODP Group Implementation + Guidance + +3.2.2. FFC Diffie-Hellman Using Named MODP Groups + + The diffie-hellman-group14-sha256 key exchange method is defined in + [RFC8268] and represents a key exchange that has approximately 112 + bits of security strength that matches 3des-cbc symmetric cipher + security strength. It is a reasonably simple transition from SHA-1 + to SHA-2, and given that diffie-hellman-group14-sha1 and diffie- + hellman-group14-sha256 share a MODP group and only differ in the hash + function used for the KDF and integrity, it is a correspondingly + simple transition from implementing diffie-hellman-group14-sha1 to + implementing diffie-hellman-group14-sha256. Given that diffie- + hellman-group14-sha1 is being removed from mandatory to implement + (MTI) status, the diffie-hellman-group14-sha256 method MUST be + implemented. The rest of the FFC MODP group from [RFC8268] have a + larger number of security bits and are suitable for symmetric ciphers + that also have a similar number of security bits. + + Table 10 provides explicit guidance by name. + + +===============================+==========+ + | Key Exchange Method Name | Guidance | + +===============================+==========+ + | diffie-hellman-group14-sha256 | MUST | + +-------------------------------+----------+ + | gss-group14-sha256-* | SHOULD | + +-------------------------------+----------+ + | diffie-hellman-group15-sha512 | MAY | + +-------------------------------+----------+ + | gss-group15-sha512-* | MAY | + +-------------------------------+----------+ + | diffie-hellman-group16-sha512 | SHOULD | + +-------------------------------+----------+ + | gss-group16-sha512-* | MAY | + +-------------------------------+----------+ + | diffie-hellman-group17-sha512 | MAY | + +-------------------------------+----------+ + | gss-group17-sha512-* | MAY | + +-------------------------------+----------+ + | diffie-hellman-group18-sha512 | MAY | + +-------------------------------+----------+ + | gss-group18-sha512-* | MAY | + +-------------------------------+----------+ + + Table 10: FFC Named Group Implementation + Guidance + +3.3. Integer Factorization Cryptography (IFC) + + The rsa1024-sha1 key exchange method is defined in [RFC4432] and uses + an RSA 1024-bit modulus with a SHA-1 hash. This key exchange does + NOT meet security requirements. This method MUST NOT be implemented. + + The rsa2048-sha256 key exchange method is defined in [RFC4432] and + uses an RSA 2048-bit modulus with a SHA2-256 hash. This key exchange + meets 112-bit minimum security strength. This method MAY be + implemented. + + Table 11 provides a summary of the guidance for IFC key exchanges. + + +==========================+==========+ + | Key Exchange Method Name | Guidance | + +==========================+==========+ + | rsa1024-sha1 | MUST NOT | + +--------------------------+----------+ + | rsa2048-sha256 | MAY | + +--------------------------+----------+ + + Table 11: IFC Implementation Guidance + +3.4. KDFs and Integrity Hashing + + The SHA-1 and SHA-2 family of hashing algorithms are combined with + the FFC, ECC, and IFC algorithms to comprise a key exchange method + name. + + The selected hash algorithm is used both in the KDF as well as for + the integrity of the response. + + All of the key exchange methods using the SHA-1 hashing algorithm + should be deprecated and phased out due to security concerns for SHA- + 1, as documented in [RFC6194]. + + Unconditionally deprecating and/or disallowing SHA-1 everywhere will + hasten the day when it may be simply removed from implementations + completely. Leaving partially broken algorithms lying around is not + a good thing to do. + + The SHA-2 family of hashes [RFC6234] is more secure than SHA-1. They + have been standardized for use in SSH with many of the currently + defined key exchanges. + + Please note that at the present time, there is no key exchange method + for Secure Shell that uses the SHA-3 family of secure hashing + functions or the Extendable-Output Functions [NIST.FIPS.202]. + + Prior to the changes made by this document, diffie-hellman- + group1-sha1 and diffie-hellman-group14-sha1 were MTI. diffie- + hellman-group14-sha1 is the stronger of the two. Group14 (a 2048-bit + MODP group) is defined in Section 3 of [RFC3526]. The SSH group1 is + defined in Section 8.1 of [RFC4253] as using the Oakley Group 2 + provided in Section 6.2 of [RFC2409] (a 1024-bit MODP group). This + group1 MODP group with approximately 80 bits of security is too weak + to be retained. However, rather than jumping from the MTI status to + making it disallowed, many implementers suggested that it should + transition to deprecated first and be disallowed at a later time. + The group14 MODP group using a SHA-1 hash for the KDF is not as weak + as the group1 MODP group. There are some legacy situations where it + will still provide administrators with value, such as small hardware + Internet of Things (IOT) devices that have insufficient compute and + memory resources to use larger MODP groups before a timeout of the + session occurs. There was consensus to transition from MTI to a + requirement status that provides for continued use with the + expectation that it would be deprecated or disallowed in the future. + Therefore, it is considered reasonable to retain the diffie-hellman- + group14-sha1 exchange for interoperability with legacy + implementations. The diffie-hellman-group14-sha1 key exchange MAY be + implemented, but should be put at the end of the list of negotiated + key exchanges. + + The diffie-hellman-group1-sha1 and diffie-hellman-group-exchange-sha1 + SHOULD NOT be implemented. The gss-group1-sha1-*, gss- + group14-sha1-*, and gss-gex-sha1-* key exchanges are already + specified as SHOULD NOT be implemented by [RFC8732]. + +3.5. Secure Shell Extension Negotiation + + There are two methods, ext-info-c and ext-info-s, defined in + [RFC8308]. They provide a mechanism to support other Secure Shell + negotiations. Being able to extend functionality is desirable. Both + ext-info-c and ext-info-s SHOULD be implemented. + +4. Summary Guidance for Implementation of Key Exchange Method Names + + Table 12 provides the existing key exchange method names listed + alphabetically. The Implement column contains the current + recommendations of this RFC. + + +=======================+============+================+===========+ + | Key Exchange Method | Reference | Previous | RFC 9142 | + | Name | | Recommendation | Implement | + +=======================+============+================+===========+ + | curve25519-sha256 | [RFC8731] | none | SHOULD | + +-----------------------+------------+----------------+-----------+ + | curve448-sha512 | [RFC8731] | none | MAY | + +-----------------------+------------+----------------+-----------+ + | diffie-hellman-group- | [RFC4419], | none | SHOULD | + | exchange-sha1 | [RFC8270] | | NOT | + +-----------------------+------------+----------------+-----------+ + | diffie-hellman-group- | [RFC4419], | none | MAY | + | exchange-sha256 | [RFC8270] | | | + +-----------------------+------------+----------------+-----------+ + | diffie-hellman- | [RFC4253] | MUST | SHOULD | + | group1-sha1 | | | NOT | + +-----------------------+------------+----------------+-----------+ + | diffie-hellman- | [RFC4253] | MUST | MAY | + | group14-sha1 | | | | + +-----------------------+------------+----------------+-----------+ + | diffie-hellman- | [RFC8268] | none | MUST | + | group14-sha256 | | | | + +-----------------------+------------+----------------+-----------+ + | diffie-hellman- | [RFC8268] | none | MAY | + | group15-sha512 | | | | + +-----------------------+------------+----------------+-----------+ + | diffie-hellman- | [RFC8268] | none | SHOULD | + | group16-sha512 | | | | + +-----------------------+------------+----------------+-----------+ + | diffie-hellman- | [RFC8268] | none | MAY | + | group17-sha512 | | | | + +-----------------------+------------+----------------+-----------+ + | diffie-hellman- | [RFC8268] | none | MAY | + | group18-sha512 | | | | + +-----------------------+------------+----------------+-----------+ + | ecdh-sha2-* | [RFC5656] | MAY | MAY | + +-----------------------+------------+----------------+-----------+ + | ecdh-sha2-nistp256 | [RFC5656] | MUST | SHOULD | + +-----------------------+------------+----------------+-----------+ + | ecdh-sha2-nistp384 | [RFC5656] | MUST | SHOULD | + +-----------------------+------------+----------------+-----------+ + | ecdh-sha2-nistp521 | [RFC5656] | MUST | SHOULD | + +-----------------------+------------+----------------+-----------+ + | ecmqv-sha2 | [RFC5656] | MAY | MAY | + +-----------------------+------------+----------------+-----------+ + | ext-info-c | [RFC8308] | SHOULD | SHOULD | + +-----------------------+------------+----------------+-----------+ + | ext-info-s | [RFC8308] | SHOULD | SHOULD | + +-----------------------+------------+----------------+-----------+ + | gss- | [RFC4462] | reserved | reserved | + +-----------------------+------------+----------------+-----------+ + | gss- | [RFC8732] | SHOULD | SHOULD | + | curve25519-sha256-* | | | | + +-----------------------+------------+----------------+-----------+ + | gss-curve448-sha512-* | [RFC8732] | MAY | MAY | + +-----------------------+------------+----------------+-----------+ + | gss-gex-sha1-* | [RFC4462], | SHOULD NOT | SHOULD | + | | [RFC8732] | | NOT | + +-----------------------+------------+----------------+-----------+ + | gss-group1-sha1-* | [RFC4462], | SHOULD NOT | SHOULD | + | | [RFC8732] | | NOT | + +-----------------------+------------+----------------+-----------+ + | gss-group14-sha1-* | [RFC4462], | SHOULD NOT | SHOULD | + | | [RFC8732] | | NOT | + +-----------------------+------------+----------------+-----------+ + | gss-group14-sha256-* | [RFC8732] | SHOULD | SHOULD | + +-----------------------+------------+----------------+-----------+ + | gss-group15-sha512-* | [RFC8732] | MAY | MAY | + +-----------------------+------------+----------------+-----------+ + | gss-group16-sha512-* | [RFC8732] | SHOULD | MAY | + +-----------------------+------------+----------------+-----------+ + | gss-group17-sha512-* | [RFC8732] | MAY | MAY | + +-----------------------+------------+----------------+-----------+ + | gss-group18-sha512-* | [RFC8732] | MAY | MAY | + +-----------------------+------------+----------------+-----------+ + | gss-nistp256-sha256-* | [RFC8732] | SHOULD | SHOULD | + +-----------------------+------------+----------------+-----------+ + | gss-nistp384-sha384-* | [RFC8732] | MAY | SHOULD | + +-----------------------+------------+----------------+-----------+ + | gss-nistp521-sha512-* | [RFC8732] | MAY | SHOULD | + +-----------------------+------------+----------------+-----------+ + | rsa1024-sha1 | [RFC4432] | MAY | MUST NOT | + +-----------------------+------------+----------------+-----------+ + | rsa2048-sha256 | [RFC4432] | MAY | MAY | + +-----------------------+------------+----------------+-----------+ + + Table 12: IANA Guidance for Implementation of Key Exchange + Method Names + + The full set of official [IANA-SSH] "Key Exchange Method Names" not + otherwise mentioned in this document MAY be implemented. + +5. Security Considerations + + This SSH protocol provides a secure encrypted channel over an + insecure network. It performs server host authentication, key + exchange, encryption, and integrity checks. It also derives a unique + session ID that may be used by higher-level protocols. The key + exchange itself generates a shared secret and uses the hash function + for both the KDF and integrity. + + Full security considerations for this protocol are provided in + [RFC4251] and continue to apply. In addition, the security + considerations provided in [RFC4432] apply. Note that Forward + Secrecy is NOT available with the rsa1024-sha1 or rsa2048-sha256 key + exchanges. + + It is desirable to deprecate or disallow key exchange methods that + are considered weak so they are not still actively in operation when + they are broken. + + A key exchange method is considered weak when the security strength + is insufficient to match the symmetric cipher or the algorithm has + been broken. + + The 1024-bit MODP group used by diffie-hellman-group1-sha1 is too + small for the symmetric ciphers used in SSH. + + MODP groups with a modulus size less than 2048 bits are too small for + the symmetric ciphers used in SSH. If the diffie-hellman-group- + exchange-sha256 or diffie-hellman-group-exchange-sha1 key exchange + method is used, the modulus size of the MODP group used needs to be + at least 2048 bits. + + At this time, the rsa1024-sha1 key exchange is too small for the + symmetric ciphers used in SSH. + + The use of SHA-1 for use with any key exchange may not yet be + completely broken, but it is time to retire all uses of this + algorithm as soon as possible. + + The diffie-hellman-group14-sha1 algorithm is not yet completely + deprecated. This is to provide a practical transition from the MTI + algorithms to a new one. However, it would be best to only be used + as a last resort in key exchange negotiations. All key exchange + methods using the SHA-1 hash are to be considered as deprecated. + +6. IANA Considerations + + IANA has added a new column to the "Key Exchange Method Names" + registry [IANA-SSH] with the heading "OK to Implement" and annotated + entries therein with the implementation guidance provided in + Section 4, "Summary Guidance for Implementation of Key Exchange + Method Names", in this document. IANA also added entries for ecdh- + sha2-nistp256, ecdh-sha2-nistp384, and ecdh-sha2-nistp521, and added + references to [RFC4462] and [RFC8732] for gss-gex-sha1-*, gss- + group1-sha1-*, gss-group14-sha1-*, diffie-hellman-group-exchange- + sha1, and diffie-hellman-group-exchange-sha256. A summary may be + found in Table 12 in Section 4. IANA has also included this document + as an additional registry reference for the suggested implementation + guidance provided in Section 4 of this document and added a note + indicating the following: + + | OK to Implement guidance entries for registrations that pre-date + | [RFC9142] are found in Table 12 in Section 4 of [RFC9142]. + + Registry entries annotated with "MUST NOT" are considered disallowed. + Registry entries annotated with "SHOULD NOT" are deprecated and may + be disallowed in the future. + +7. References + +7.1. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH) + Protocol Assigned Numbers", RFC 4250, + DOI 10.17487/RFC4250, January 2006, + . + + [RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253, + January 2006, . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [RFC8268] Baushke, M., "More Modular Exponentiation (MODP) Diffie- + Hellman (DH) Key Exchange (KEX) Groups for Secure Shell + (SSH)", RFC 8268, DOI 10.17487/RFC8268, December 2017, + . + + [RFC8270] Velvindron, L. and M. Baushke, "Increase the Secure Shell + Minimum Recommended Diffie-Hellman Modulus Size to 2048 + Bits", RFC 8270, DOI 10.17487/RFC8270, December 2017, + . + + [RFC8308] Bider, D., "Extension Negotiation in the Secure Shell + (SSH) Protocol", RFC 8308, DOI 10.17487/RFC8308, March + 2018, . + + [RFC8731] Adamantiadis, A., Josefsson, S., and M. Baushke, "Secure + Shell (SSH) Key Exchange Method Using Curve25519 and + Curve448", RFC 8731, DOI 10.17487/RFC8731, February 2020, + . + +7.2. Informative References + + [IANA-SSH] IANA, "Secure Shell (SSH) Protocol Parameters", + . + + [NIST.FIPS.202] + National Institute of Standards and Technology, "SHA-3 + Standard: Permutation-Based Hash and Extendable-Output + Functions", FIPS PUB 202, DOI 10.6028/NIST.FIPS.202, + August 2015, . + + [NIST.SP.800-107r1] + Dang, Q., "Recommendation for applications using approved + hash algorithms", DOI 10.6028/NIST.SP.800-107r1, August + 2012, . + + [NIST.SP.800-57pt1r5] + Barker, E., "Recommendation for Key Management: Part 1 - + General", DOI 10.6028/NIST.SP.800-57pt1r5, May 2020, + . + + [RFC2409] Harkins, D. and D. Carrel, "The Internet Key Exchange + (IKE)", RFC 2409, DOI 10.17487/RFC2409, November 1998, + . + + [RFC3526] Kivinen, T. and M. Kojo, "More Modular Exponential (MODP) + Diffie-Hellman groups for Internet Key Exchange (IKE)", + RFC 3526, DOI 10.17487/RFC3526, May 2003, + . + + [RFC4086] Eastlake 3rd, D., Schiller, J., and S. Crocker, + "Randomness Requirements for Security", BCP 106, RFC 4086, + DOI 10.17487/RFC4086, June 2005, + . + + [RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Protocol Architecture", RFC 4251, DOI 10.17487/RFC4251, + January 2006, . + + [RFC4419] Friedl, M., Provos, N., and W. Simpson, "Diffie-Hellman + Group Exchange for the Secure Shell (SSH) Transport Layer + Protocol", RFC 4419, DOI 10.17487/RFC4419, March 2006, + . + + [RFC4432] Harris, B., "RSA Key Exchange for the Secure Shell (SSH) + Transport Layer Protocol", RFC 4432, DOI 10.17487/RFC4432, + March 2006, . + + [RFC4462] Hutzelman, J., Salowey, J., Galbraith, J., and V. Welch, + "Generic Security Service Application Program Interface + (GSS-API) Authentication and Key Exchange for the Secure + Shell (SSH) Protocol", RFC 4462, DOI 10.17487/RFC4462, May + 2006, . + + [RFC5656] Stebila, D. and J. Green, "Elliptic Curve Algorithm + Integration in the Secure Shell Transport Layer", + RFC 5656, DOI 10.17487/RFC5656, December 2009, + . + + [RFC6194] Polk, T., Chen, L., Turner, S., and P. Hoffman, "Security + Considerations for the SHA-0 and SHA-1 Message-Digest + Algorithms", RFC 6194, DOI 10.17487/RFC6194, March 2011, + . + + [RFC6234] Eastlake 3rd, D. and T. Hansen, "US Secure Hash Algorithms + (SHA and SHA-based HMAC and HKDF)", RFC 6234, + DOI 10.17487/RFC6234, May 2011, + . + + [RFC7748] Langley, A., Hamburg, M., and S. Turner, "Elliptic Curves + for Security", RFC 7748, DOI 10.17487/RFC7748, January + 2016, . + + [RFC8031] Nir, Y. and S. Josefsson, "Curve25519 and Curve448 for the + Internet Key Exchange Protocol Version 2 (IKEv2) Key + Agreement", RFC 8031, DOI 10.17487/RFC8031, December 2016, + . + + [RFC8732] Sorce, S. and H. Kario, "Generic Security Service + Application Program Interface (GSS-API) Key Exchange with + SHA-2", RFC 8732, DOI 10.17487/RFC8732, February 2020, + . + + [TRANSCRIPTION] + Bhargavan, K. and G. Leurent, "Transcript Collision + Attacks: Breaking Authentication in TLS, IKE, and SSH", + Network and Distributed System Security Symposium (NDSS), + DOI 10.14722/ndss.2016.23418, February 2016, + . + +Acknowledgements + + Thanks to the following people for review and comments: Denis Bider, + Peter Gutmann, Damien Miller, Niels Moeller, Matt Johnston, Iwamoto + Kouichi, Simon Josefsson, Dave Dugal, Daniel Migault, Anna Johnston, + Tero Kivinen, and Travis Finkenauer. + + Thanks to the following people for code to implement interoperable + exchanges using some of these groups as found in this document: + Darren Tucker for OpenSSH and Matt Johnston for Dropbear. And thanks + to Iwamoto Kouichi for information about RLogin, Tera Term (ttssh), + and Poderosa implementations also adopting new Diffie-Hellman groups + based on this document. + +Author's Address + + Mark D. Baushke + + Email: mbaushke.ietf@gmail.com diff --git a/specifications/rfc9519.txt b/specifications/rfc9519.txt new file mode 100644 index 0000000..3da643a --- /dev/null +++ b/specifications/rfc9519.txt @@ -0,0 +1,272 @@ + + + + +Internet Engineering Task Force (IETF) P. Yee +Request for Comments: 9519 AKAYLA +Updates: 4250, 4716, 4819, 8308 January 2024 +Category: Standards Track +ISSN: 2070-1721 + + + Update to the IANA SSH Protocol Parameters Registry Requirements + +Abstract + + This specification updates the registration policies for adding new + entries to registries within the IANA "Secure Shell (SSH) Protocol + Parameters" group of registries. Previously, the registration policy + was generally IETF Review, as defined in RFC 8126, although a few + registries require Standards Action. This specification changes it + from IETF Review to Expert Review. This document updates RFCs 4250, + 4716, 4819, and 8308. + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 7841. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + https://www.rfc-editor.org/info/rfc9519. + +Copyright Notice + + Copyright (c) 2024 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (https://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Revised BSD License text as described in Section 4.e of the + Trust Legal Provisions and are provided without warranty as described + in the Revised BSD License. + +Table of Contents + + 1. Introduction + 1.1. Requirements Language + 2. SSH Protocol Parameters Affected + 3. Designated Expert Pool + 4. IANA Considerations + 5. Security Considerations + 6. References + 6.1. Normative References + 6.2. Informative References + Acknowledgements + Author's Address + +1. Introduction + + The IANA "Secure Shell (SSH) Protocol Parameters" registry was + populated by several RFCs including [RFC4250], [RFC4716], [RFC4819], + and [RFC8308]. Outside of some narrow value ranges that require + Standards Action in order to add new values or that are marked for + Private Use, the registration policy for other portions of the + registry was IETF Review [RFC8126]. This specification changes the + policy from IETF Review to Expert Review. This change is in line + with similar changes undertaken for certain IPsec and TLS registries. + +1.1. Requirements Language + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + +2. SSH Protocol Parameters Affected + + The following table lists the "Secure Shell (SSH) Protocol + Parameters" registries whose registration policy has changed from + IETF Review to Expert Review. Where this change applied to a + specific range of values within the particular parameter, that range + is given in the notes column. Affected registries now list this + document as a reference. + + +===============================+===========+=======================+ + | Parameter Name | RFC | Notes | + +===============================+===========+=======================+ + | Authentication Method | [RFC4250] | | + | Names | | | + +-------------------------------+-----------+-----------------------+ + | Channel Connection | [RFC4250] | 0x00000001-0xFDFFFFFF | + | Failure Reason Codes | | (inclusive) | + | and Descriptions | | | + +-------------------------------+-----------+-----------------------+ + | Compression Algorithm | [RFC4250] | | + | Names | | | + +-------------------------------+-----------+-----------------------+ + | Connection Protocol | [RFC4250] | | + | Channel Request Names | | | + +-------------------------------+-----------+-----------------------+ + | Connection Protocol | [RFC4250] | | + | Channel Types | | | + +-------------------------------+-----------+-----------------------+ + | Connection Protocol | [RFC4250] | | + | Global Request Names | | | + +-------------------------------+-----------+-----------------------+ + | Connection Protocol | [RFC4250] | | + | Subsystem Names | | | + +-------------------------------+-----------+-----------------------+ + | Disconnection Messages | [RFC4250] | 0x00000001-0xFDFFFFFF | + | Reason Codes and | | (inclusive) | + | Descriptions | | | + +-------------------------------+-----------+-----------------------+ + | Encryption Algorithm | [RFC4250] | | + | Names | | | + +-------------------------------+-----------+-----------------------+ + | Extended Channel Data | [RFC4250] | 0x00000001-0xFDFFFFFF | + | Transfer data_type_code | | (inclusive) | + | and Data | | | + +-------------------------------+-----------+-----------------------+ + | Extension Names | [RFC8308] | | + +-------------------------------+-----------+-----------------------+ + | Key Exchange Method | [RFC4250] | | + | Names | | | + +-------------------------------+-----------+-----------------------+ + | MAC Algorithm Names | [RFC4250] | | + +-------------------------------+-----------+-----------------------+ + | Pseudo-Terminal Encoded | [RFC4250] | | + | Terminal Modes | | | + +-------------------------------+-----------+-----------------------+ + | Public Key Algorithm | [RFC4250] | | + | Names | | | + +-------------------------------+-----------+-----------------------+ + | Publickey Subsystem | [RFC4819] | | + | Attributes | | | + +-------------------------------+-----------+-----------------------+ + | Publickey Subsystem | [RFC4819] | | + | Request Names | | | + +-------------------------------+-----------+-----------------------+ + | Publickey Subsystem | [RFC4819] | | + | Response Names | | | + +-------------------------------+-----------+-----------------------+ + | Service Names | [RFC4250] | | + +-------------------------------+-----------+-----------------------+ + | Signal Names | [RFC4250] | | + +-------------------------------+-----------+-----------------------+ + | SSH Public-Key File | [RFC4716] | Excluding header-tags | + | Header Tags | | beginning with x- | + +-------------------------------+-----------+-----------------------+ + + Table 1: Secure Shell (SSH) Protocol Parameters Affected + + The only IANA SSH protocol parameter registries not affected are + "Message Numbers" and "Publickey Subsystem Status Codes", as these + remain Standards Action due to their limited resources as one-byte + registry values. + +3. Designated Expert Pool + + Expert Review [RFC8126] registry requests are registered after a + three-week review period on the mailing + list, and on the advice of one or more designated experts. However, + to allow for the allocation of values prior to publication, the + designated experts may approve registration once they are satisfied + that such a specification will be published. + + Registration requests sent to the mailing list for review SHOULD use + an appropriate subject (e.g., "Request to register value in SSH + protocol parameters registry"). + + Within the review period, the designated experts will either approve + or deny the registration request, communicating this decision to the + review list and IANA. Denials MUST include an explanation and, if + applicable, suggestions as to how to make the request successful. + Registration requests that are undetermined for a period longer than + 21 days can be brought to the IESG's attention (using the + mailing list) for resolution. + + Criteria that SHOULD be applied by the designated experts includes + determining whether the proposed registration duplicates existing + functionality (which is not permitted), whether it is likely to be of + general applicability or useful only for a single application, and + whether the registration description is clear. + + IANA MUST only accept registry updates from the designated experts + and the IESG. It SHOULD direct all requests for registration from + other sources to the review mailing list. + + It is suggested that multiple designated experts be appointed who are + able to represent the perspectives of different applications using + this specification, in order to enable broadly informed review of + registration decisions. In cases where a registration decision could + be perceived as creating a conflict of interest for a particular + expert, that expert SHOULD defer to the judgment of the other + experts. + +4. IANA Considerations + + This memo is entirely about updating the IANA "Secure Shell (SSH) + Protocol Parameters" registry. + +5. Security Considerations + + This memo does not change the Security Considerations for any of the + updated RFCs. + +6. References + +6.1. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH) + Protocol Assigned Numbers", RFC 4250, + DOI 10.17487/RFC4250, January 2006, + . + + [RFC4819] Galbraith, J., Van Dyke, J., and J. Bright, "Secure Shell + Public Key Subsystem", RFC 4819, DOI 10.17487/RFC4819, + March 2007, . + + [RFC8126] Cotton, M., Leiba, B., and T. Narten, "Guidelines for + Writing an IANA Considerations Section in RFCs", BCP 26, + RFC 8126, DOI 10.17487/RFC8126, June 2017, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [RFC8308] Bider, D., "Extension Negotiation in the Secure Shell + (SSH) Protocol", RFC 8308, DOI 10.17487/RFC8308, March + 2018, . + +6.2. Informative References + + [CURDLE-MA] + Turner, S., "Subject: [Curdle] Time to Review IANA SSH + Registries Policies?", message to the Curdle mailing list, + February 2021, + . + + [RFC4716] Galbraith, J. and R. Thayer, "The Secure Shell (SSH) + Public Key File Format", RFC 4716, DOI 10.17487/RFC4716, + November 2006, . + +Acknowledgements + + The impetus for this specification was a February 2021 discussion on + the CURDLE mailing list [CURDLE-MA]. + +Author's Address + + Peter E. Yee + AKAYLA + Mountain View, CA 94043 + United States of America + Email: peter@akayla.com diff --git a/src/bin/hush.rs b/src/bin/hush.rs new file mode 100644 index 0000000..ae0964b --- /dev/null +++ b/src/bin/hush.rs @@ -0,0 +1,39 @@ +use error_stack::ResultExt; +use hush::config::{BasicClientConfiguration, ClientConfiguration}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let mut base_config = match BasicClientConfiguration::new(std::env::args()) { + Ok(config) => config, + Err(e) => { + eprintln!("ERROR: {}", e); + return; + } + }; + + if let Err(e) = base_config.establish_subscribers() { + eprintln!("ERROR: could not set up logging infrastructure: {}", e); + std::process::exit(2); + } + + let runtime = match base_config.configured_runtime() { + Ok(runtime) => runtime, + Err(e) => { + tracing::error!(%e, "could not start system runtime"); + std::process::exit(3); + } + }; + + let result = runtime.block_on(async move { + tracing::info!("Starting Hush"); + match ClientConfiguration::try_from(base_config).await { + Ok(config) => hush::client::hush(config).await, + Err(e) => Err(e).change_context(hush::OperationalError::ConfigurationError), + } + }); + + if let Err(e) = result { + tracing::error!("{}", e); + std::process::exit(1); + } +} diff --git a/src/bin/hushd.rs b/src/bin/hushd.rs new file mode 100644 index 0000000..e385c00 --- /dev/null +++ b/src/bin/hushd.rs @@ -0,0 +1,4 @@ +#[cfg(not(tarpaulin_include))] +fn main() { + unimplemented!(); +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..fbe85dc --- /dev/null +++ b/src/client.rs @@ -0,0 +1,164 @@ +use crate::config::{ClientCommand, ClientConfiguration}; +use crate::crypto::{ + CompressionAlgorithm, EncryptionAlgorithm, HostKeyAlgorithm, KeyExchangeAlgorithm, MacAlgorithm, +}; +use crate::network::host::Host; +use crate::ssh; +use crate::OperationalError; +use error_stack::{report, Report, ResultExt}; +use std::fmt::Display; +use std::str::FromStr; +use thiserror::Error; + +pub async fn hush(base_config: ClientConfiguration) -> error_stack::Result<(), OperationalError> { + match base_config.command() { + ClientCommand::ListMacAlgorithms => print_options(MacAlgorithm::allowed()), + + ClientCommand::ListHostKeyAlgorithms => print_options(HostKeyAlgorithm::allowed()), + + ClientCommand::ListEncryptionAlgorithms => print_options(EncryptionAlgorithm::allowed()), + + ClientCommand::ListKeyExchangeAlgorithms => print_options(KeyExchangeAlgorithm::allowed()), + + ClientCommand::ListCompressionAlgorithms => print_options(CompressionAlgorithm::allowed()), + + ClientCommand::Connect { target } => connect(&base_config, target).await, + } +} + +struct Target { + username: String, + host: Host, + port: u16, +} + +#[derive(Debug, Error)] +pub enum TargetParseError { + #[error("Invalid port number '{port_string}': {error}")] + InvalidPort { + port_string: String, + error: std::num::ParseIntError, + }, + #[error("Invalid hostname '{hostname}': {error}")] + InvalidHostname { + hostname: String, + error: crate::network::host::HostParseError, + }, +} + +impl FromStr for Target { + type Err = Report; + + fn from_str(s: &str) -> Result { + let (username, host_and_port) = match s.split_once('@') { + None => (whoami::username(), s), + Some((username, rest)) => (username.to_string(), rest), + }; + + let (port, host) = match host_and_port.rsplit_once(':') { + None => (22, host_and_port), + Some((host, portstr)) => match u16::from_str(portstr) { + Ok(port) => (port, host), + Err(error) => { + return Err(report!(TargetParseError::InvalidPort { + port_string: portstr.to_string(), + error, + }) + .attach_printable(format!("from target string {:?}", s))) + } + }, + }; + + let host = Host::from_str(host) + .map_err(|error| { + report!(TargetParseError::InvalidHostname { + hostname: host.to_string(), + error, + }) + }) + .attach_printable(format!("from target string {:?}", s))?; + + Ok(Target { + username, + host, + port, + }) + } +} + +async fn connect( + base_config: &ClientConfiguration, + target: &str, +) -> error_stack::Result<(), OperationalError> { + let resolver = base_config.resolver(); + let target = Target::from_str(target) + .change_context(OperationalError::UnableToParseHostAddress) + .attach_printable_lazy(|| format!("target address '{}'", target))?; + let mut stream = target + .host + .connect(resolver, 22) + .await + .change_context(OperationalError::Connection)?; + + let _preamble = ssh::Preamble::read(&mut stream) + .await + .change_context(OperationalError::Connection)?; + + // if !commentary.is_empty() { + // tracing::debug!(?commentary, "Server sent commentary."); + // } + // if !pre_message.is_empty() { + // for line in pre_message.lines() { + // tracing::debug!(?line, "Server sent prefix line."); + // } + // } + // + // let my_info = format!( + // "SSH-2.0-{}_{}\r\n", + // env!("CARGO_PKG_NAME"), + // env!("CARGO_PKG_VERSION") + // ); + // connection + // .write_all(my_info.as_bytes()) + // .await + // .map_err(OperationalError::WriteBanner)?; + // + // assert_eq!(4096, read_buffer.len()); + // read_buffer.fill(0); + // + // let mut stream = SshChannel::new(connection); + // let mut rng = rand::thread_rng(); + // + // let packet = stream + // .read() + // .await + // .map_err(|error| OperationalError::Read { + // context: "Initial key exchange read", + // error, + // })? + // .expect("has initial packet"); + // let message_type = SshMessageID::from(packet.buffer[0]); + // tracing::debug!(size=packet.buffer.len(), %message_type, "Initial buffer received."); + // let keyx = SshKeyExchange::try_from(packet)?; + // println!("{:?}", keyx); + // + // let client_config = config.settings_for(""); + // let my_kex = SshKeyExchange::new(&mut rng, client_config)?; + // stream + // .write(my_kex.into()) + // .await + // .map_err(|error| OperationalError::Write { + // context: "initial negotiation message", + // error, + // })?; + + Ok(()) +} + +fn print_options(items: &[T]) -> error_stack::Result<(), OperationalError> { + for item in items.iter() { + println!("{}", item); + } + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e1d9969 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,298 @@ +mod command_line; +mod config_file; +mod console; +mod error; +mod logging; +mod resolver; + +use crate::config::console::ConsoleConfiguration; +use crate::config::logging::LoggingConfiguration; +use crate::crypto::known_algorithms::{ + ALLOWED_COMPRESSION_ALGORITHMS, ALLOWED_ENCRYPTION_ALGORITHMS, ALLOWED_HOST_KEY_ALGORITHMS, + ALLOWED_KEY_EXCHANGE_ALGORITHMS, ALLOWED_MAC_ALGORITHMS, +}; +use crate::crypto::{ + CompressionAlgorithm, EncryptionAlgorithm, HostKeyAlgorithm, KeyExchangeAlgorithm, MacAlgorithm, +}; +use crate::encodings::ssh::{load_openssh_file_keys, PublicKey}; +use clap::Parser; +use config_file::ConfigFile; +use error_stack::ResultExt; +use hickory_resolver::TokioAsyncResolver; +use proptest::arbitrary::Arbitrary; +use proptest::strategy::{BoxedStrategy, Strategy}; +use std::collections::{HashMap, HashSet}; +use std::ffi::OsString; +use std::str::FromStr; +use tokio::runtime::Runtime; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::prelude::*; + +pub use self::command_line::ClientCommand; +use self::command_line::CommandLineArguments; +pub use self::error::ConfigurationError; + +pub struct BasicClientConfiguration { + runtime: RuntimeConfiguration, + logging: LoggingConfiguration, + config_file: Option, + command: ClientCommand, +} + +impl BasicClientConfiguration { + /// Load a basic client configuration for this run. + /// + /// This will parse the process's command line arguments, and parse + /// a config file if given, but will not interpret this information + /// beyond that required to understand the user's goals for the + /// runtime system and logging. For this reason, it is not async, + /// as it is responsible for determining all the information we + /// will use to generate the runtime. + pub fn new(args: I) -> Result + where + I: IntoIterator, + T: Into + Clone, + { + let mut command_line_arguments = CommandLineArguments::try_parse_from(args)?; + let mut config_file = ConfigFile::new(command_line_arguments.config_file.take())?; + let mut runtime = RuntimeConfiguration::default(); + let mut logging = LoggingConfiguration::default(); + + // we prefer the command line to the config file, so first merge + // in the config file so that when we later merge the command line, + // it overwrites any config file options. + if let Some(config_file) = config_file.as_mut() { + config_file.merge_standard_options_into(&mut runtime, &mut logging); + } + command_line_arguments.merge_standard_options_into(&mut runtime, &mut logging); + + Ok(BasicClientConfiguration { + runtime, + logging, + config_file, + command: command_line_arguments.command, + }) + } + + /// Set up the tracing subscribers based on the config file and command line options. + /// + /// This will definitely set up our logging substrate, but may also create a subscriber + /// for the console. + #[cfg(not(tarpaulin_include))] + pub fn establish_subscribers(&mut self) -> Result<(), std::io::Error> { + let tracing_layer = self.logging.layer()?; + let mut layers = vec![tracing_layer]; + + if let Some(console_config) = self.runtime.console.take() { + layers.push(console_config.layer()); + } + + tracing_subscriber::registry().with(layers).init(); + + Ok(()) + } + + /// Generate a new tokio runtime based on the configuration / command line options + /// provided. + #[cfg(not(tarpaulin_include))] + pub fn configured_runtime(&mut self) -> Result { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .max_blocking_threads(self.runtime.tokio_blocking_threads) + .worker_threads(self.runtime.tokio_worker_threads) + .build() + } +} + +#[derive(Debug)] +pub struct ClientConnectionOpts { + pub key_exchange_algorithms: Vec, + pub server_host_key_algorithms: Vec, + pub encryption_algorithms: Vec, + pub mac_algorithms: Vec, + pub compression_algorithms: Vec, + pub languages: Vec, + pub predict: Option, +} + +impl Default for ClientConnectionOpts { + fn default() -> Self { + ClientConnectionOpts { + key_exchange_algorithms: vec![KeyExchangeAlgorithm::Curve25519Sha256], + server_host_key_algorithms: vec![HostKeyAlgorithm::Ed25519], + encryption_algorithms: vec![EncryptionAlgorithm::Aes256Ctr], + mac_algorithms: vec![MacAlgorithm::HmacSha256], + compression_algorithms: vec![], + languages: vec![], + predict: None, + } + } +} + +impl Arbitrary for ClientConnectionOpts { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + let keyx = proptest::sample::select(ALLOWED_KEY_EXCHANGE_ALGORITHMS); + let hostkey = proptest::sample::select(ALLOWED_HOST_KEY_ALGORITHMS); + let enc = proptest::sample::select(ALLOWED_ENCRYPTION_ALGORITHMS); + let mac = proptest::sample::select(ALLOWED_MAC_ALGORITHMS); + let comp = proptest::sample::select(ALLOWED_COMPRESSION_ALGORITHMS); + + ( + proptest::collection::hash_set(keyx.clone(), 1..ALLOWED_KEY_EXCHANGE_ALGORITHMS.len()), + proptest::collection::hash_set(hostkey, 1..ALLOWED_HOST_KEY_ALGORITHMS.len()), + proptest::collection::hash_set(enc, 1..ALLOWED_ENCRYPTION_ALGORITHMS.len()), + proptest::collection::hash_set(mac, 1..ALLOWED_MAC_ALGORITHMS.len()), + proptest::collection::hash_set(comp, 1..ALLOWED_COMPRESSION_ALGORITHMS.len()), + proptest::option::of(keyx), + ) + .prop_map(|(kex, host, enc, mac, comp, pred)| ClientConnectionOpts { + key_exchange_algorithms: finalize_options(kex).unwrap(), + server_host_key_algorithms: finalize_options(host).unwrap(), + encryption_algorithms: finalize_options(enc).unwrap(), + mac_algorithms: finalize_options(mac).unwrap(), + compression_algorithms: finalize_options(comp).unwrap(), + languages: vec![], + predict: pred.and_then(|x| KeyExchangeAlgorithm::from_str(x).ok()), + }) + .boxed() + } +} + +fn finalize_options(inputs: HashSet<&str>) -> Result, E> +where + T: FromStr, +{ + let mut result = vec![]; + + for item in inputs.into_iter() { + let item = T::from_str(item)?; + result.push(item) + } + + Ok(result) +} + +#[derive(Default)] +pub struct ServerConfiguration { + runtime: RuntimeConfiguration, + logging: LoggingConfiguration, +} + +pub struct RuntimeConfiguration { + tokio_worker_threads: usize, + tokio_blocking_threads: usize, + console: Option, +} + +impl Default for RuntimeConfiguration { + fn default() -> Self { + RuntimeConfiguration { + tokio_worker_threads: 4, + tokio_blocking_threads: 16, + console: None, + } + } +} + +impl ServerConfiguration { + /// Load a server configuration for this run. + /// + /// This will parse the process's command line arguments, and parse + /// a config file if given, so it can take awhile. Even though this + /// function does a bunch of IO, it is not async, because it is expected + /// ro run before we have a tokio runtime fully established. (This + /// function will determine a bunch of things related to the runtime, + /// like how many threads to run, what tracing subscribers to include, + /// etc.) + pub fn new() -> Result { + let mut new_configuration = Self::default(); + let mut command_line = CommandLineArguments::parse(); + let mut config_file = ConfigFile::new(command_line.config_file.take())?; + + // we prefer the command line to the config file, so first merge + // in the config file so that when we later merge the command line, + // it overwrites any config file options. + if let Some(config_file) = config_file.as_mut() { + config_file.merge_standard_options_into( + &mut new_configuration.runtime, + &mut new_configuration.logging, + ); + } + command_line.merge_standard_options_into( + &mut new_configuration.runtime, + &mut new_configuration.logging, + ); + + Ok(new_configuration) + } +} + +impl BasicClientConfiguration {} + +pub struct ClientConfiguration { + _runtime: RuntimeConfiguration, + _logging: LoggingConfiguration, + resolver: TokioAsyncResolver, + _ssh_keys: HashMap, + _defaults: ClientConnectionOpts, + command: ClientCommand, +} + +impl ClientConfiguration { + pub async fn try_from( + mut basic: BasicClientConfiguration, + ) -> error_stack::Result { + let mut _ssh_keys = HashMap::new(); + let mut _defaults = ClientConnectionOpts::default(); + let resolver = basic + .config_file + .as_mut() + .and_then(|x| x.resolver.take()) + .unwrap_or_default() + .resolver() + .change_context(ConfigurationError::Resolver)?; + let user_ssh_keys = basic + .config_file + .map(|cf| cf.keys) + .unwrap_or_default(); + + tracing::info!( + provided_ssh_keys = user_ssh_keys.len(), + "loading user-provided SSH keys" + ); + for (key_name, key_info) in user_ssh_keys.into_iter() { + let _public_keys = PublicKey::load(key_info.public).await.unwrap(); + tracing::info!(?key_name, "public keys loaded"); + let _private_key = load_openssh_file_keys(key_info.private, &key_info.password) + .await + .change_context(ConfigurationError::PrivateKey)?; + tracing::info!(?key_name, "private keys loaded"); + } + + Ok(ClientConfiguration { + _runtime: basic.runtime, + _logging: basic.logging, + resolver, + _ssh_keys, + _defaults, + command: basic.command, + }) + } + + pub fn command(&self) -> &ClientCommand { + &self.command + } + + pub fn resolver(&self) -> &TokioAsyncResolver { + &self.resolver + } +} + +pub enum SshKey { + Ed25519 {}, + Ecdsa {}, + Rsa {}, +} diff --git a/src/config/command_line.rs b/src/config/command_line.rs new file mode 100644 index 0000000..973463b --- /dev/null +++ b/src/config/command_line.rs @@ -0,0 +1,205 @@ +use crate::config::logging::{LogTarget, LoggingConfiguration}; +use crate::config::{ConsoleConfiguration, RuntimeConfiguration}; +use clap::{Parser, Subcommand}; +use std::net::SocketAddr; +use std::path::PathBuf; +#[cfg(test)] +use std::str::FromStr; +use tracing_core::LevelFilter; + +#[derive(Parser, Debug, Default)] +#[command(version, about, long_about = None)] +pub struct CommandLineArguments { + /// The config file to use for this command. + #[arg(short, long)] + pub config_file: Option, + + /// The number of "normal" threads to use for this process. + /// + /// These are the threads that do the predominant part of the work + /// for the system. + #[arg(short, long)] + threads: Option, + + /// The number of "blocking" threads to use for this process. + /// + /// These threads are used for long-running operations, or operations + /// that require extensive interaction with non-asynchronous-friendly + /// IO. This should definitely be >= 1, but does not need to be super + /// high. + #[arg(short, long)] + blocking_threads: Option, + + /// The place to send log data to. + #[arg(short = 'o', long)] + log_file: Option, + + /// The log level to report. + #[arg(short, long)] + log_level: Option, + + /// A network server IP address to use for tokio-console inspection. + #[arg(short = 's', long, group = "console")] + console_network_server: Option, + + /// A unix domain socket address to use for tokio-console inspection. + #[arg(short = 'u', long, group = "console")] + console_unix_socket: Option, + + /// The command we're running as the client + #[command(subcommand)] + pub command: ClientCommand, +} + +#[derive(Debug, Default, Subcommand)] +pub enum ClientCommand { + /// List the key exchange algorithms we currently allow + #[default] + ListKeyExchangeAlgorithms, + /// List the host key algorithms we currently allow + ListHostKeyAlgorithms, + /// List the encryption algorithms we currently allow + ListEncryptionAlgorithms, + /// List the MAC algorithms we currently allow + ListMacAlgorithms, + /// List the compression algorithms we currently allow + ListCompressionAlgorithms, + /// Connect to the given host and port + Connect { target: String }, +} + +impl ClientCommand { + /// Is this a command that's just going to list some information to the console? + pub fn is_list_command(&self) -> bool { + matches!( + self, + ClientCommand::ListKeyExchangeAlgorithms + | ClientCommand::ListHostKeyAlgorithms + | ClientCommand::ListEncryptionAlgorithms + | ClientCommand::ListMacAlgorithms + | ClientCommand::ListCompressionAlgorithms + ) + } +} + +impl CommandLineArguments { + pub fn merge_standard_options_into( + &mut self, + runtime_config: &mut RuntimeConfiguration, + logging_config: &mut LoggingConfiguration, + ) { + if let Some(threads) = self.threads { + runtime_config.tokio_worker_threads = threads; + } + + if let Some(threads) = self.blocking_threads { + runtime_config.tokio_blocking_threads = threads; + } + + if let Some(log_file) = self.log_file.take() { + logging_config.target = LogTarget::File(log_file); + } + + if let Some(log_level) = self.log_level.take() { + logging_config.filter = log_level; + } else if self.command.is_list_command() { + logging_config.filter = LevelFilter::ERROR; + } + + if (self.console_network_server.is_some() || self.console_unix_socket.is_some()) + && runtime_config.console.is_none() + { + runtime_config.console = Some(ConsoleConfiguration::default()); + } + + if let Some(cns) = self.console_network_server.take() { + if let Some(x) = runtime_config.console.as_mut() { + x.server_addr = cns.into(); + } + } + + if let Some(cus) = self.console_unix_socket.take() { + if let Some(x) = runtime_config.console.as_mut() { + x.server_addr = cus.into(); + } + } + } +} + +#[cfg(test)] +fn apply_command_line( + mut cmdargs: CommandLineArguments, +) -> (RuntimeConfiguration, LoggingConfiguration) { + let mut original_runtime = RuntimeConfiguration::default(); + let mut original_logging = LoggingConfiguration::default(); + + cmdargs.merge_standard_options_into(&mut original_runtime, &mut original_logging); + + (original_runtime, original_logging) +} + +#[test] +fn command_line_wins() { + let original_runtime = RuntimeConfiguration::default(); + + let cmd = CommandLineArguments { + threads: Some(original_runtime.tokio_worker_threads + 1), + ..CommandLineArguments::default() + }; + let (test1_run, _) = apply_command_line(cmd); + assert_ne!( + original_runtime.tokio_worker_threads, + test1_run.tokio_worker_threads + ); + assert_eq!( + original_runtime.tokio_blocking_threads, + test1_run.tokio_blocking_threads + ); + + let cmd = CommandLineArguments { + blocking_threads: Some(original_runtime.tokio_blocking_threads + 1), + ..CommandLineArguments::default() + }; + let (test2_run, _) = apply_command_line(cmd); + assert_eq!( + original_runtime.tokio_worker_threads, + test2_run.tokio_worker_threads + ); + assert_ne!( + original_runtime.tokio_blocking_threads, + test2_run.tokio_blocking_threads + ); +} + +#[test] +fn can_set_console_settings() { + let cmd = CommandLineArguments { + console_network_server: Some( + SocketAddr::from_str("127.0.0.1:8080").expect("reasonable address"), + ), + ..CommandLineArguments::default() + }; + let (test1_run, _) = apply_command_line(cmd); + assert!(test1_run.console.is_some()); + assert!(matches!( + test1_run.console.unwrap().server_addr, + console_subscriber::ServerAddr::Tcp(_) + )); + + let temp_path = tempfile::NamedTempFile::new() + .expect("can build temp file") + .into_temp_path(); + std::fs::remove_file(&temp_path).unwrap(); + let filename = temp_path.to_path_buf(); + + let cmd = CommandLineArguments { + console_unix_socket: Some(filename), + ..CommandLineArguments::default() + }; + let (test2_run, _) = apply_command_line(cmd); + assert!(test2_run.console.is_some()); + assert!(matches!( + test2_run.console.unwrap().server_addr, + console_subscriber::ServerAddr::Unix(_) + )); +} diff --git a/src/config/config_file.rs b/src/config/config_file.rs new file mode 100644 index 0000000..e65dd41 --- /dev/null +++ b/src/config/config_file.rs @@ -0,0 +1,423 @@ +use crate::config::logging::{LogMode, LogTarget, LoggingConfiguration}; +use crate::config::resolver::DnsConfig; +use crate::config::RuntimeConfiguration; +use crate::crypto::known_algorithms::{ + ALLOWED_COMPRESSION_ALGORITHMS, ALLOWED_ENCRYPTION_ALGORITHMS, ALLOWED_HOST_KEY_ALGORITHMS, + ALLOWED_KEY_EXCHANGE_ALGORITHMS, ALLOWED_MAC_ALGORITHMS, +}; +use proptest::arbitrary::{any, Arbitrary}; +use proptest::strategy::{BoxedStrategy, Just, Strategy}; +use serde::de::{self, Unexpected}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::{HashMap, HashSet}; +use std::io::Read; +use std::path::PathBuf; +use thiserror::Error; +use tracing_core::Level; + +#[allow(dead_code)] +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub struct ConfigFile { + runtime: Option, + logging: Option, + pub resolver: Option, + pub keys: HashMap, + defaults: Option, + servers: HashMap, +} + +impl Arbitrary for ConfigFile { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + ( + any::>(), + any::>(), + any::>(), + keyed_section(KeyConfig::arbitrary()), + any::>(), + keyed_section(ServerConfig::arbitrary()), + ) + .prop_map( + |(runtime, logging, resolver, keys, defaults, servers)| ConfigFile { + runtime, + logging, + resolver, + keys, + defaults, + servers, + }, + ) + .boxed() + } +} + +fn keyed_section(strat: S) -> BoxedStrategy> +where + S: Strategy + 'static, +{ + proptest::collection::hash_map( + proptest::string::string_regex("[a-zA-Z0-9-]{1,30}").unwrap(), + strat, + 0..40, + ) + .boxed() +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub struct RuntimeConfig { + worker_threads: Option, + blocking_threads: Option, +} + +impl Arbitrary for RuntimeConfig { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + (any::>(), any::>()) + .prop_map(|(worker_threads, blocking_threads)| RuntimeConfig { + worker_threads: worker_threads.map(Into::into), + blocking_threads: blocking_threads.map(Into::into), + }) + .boxed() + } +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub struct LoggingConfig { + #[serde( + default, + deserialize_with = "parse_level", + serialize_with = "write_level" + )] + level: Option, + include_filename: Option, + include_lineno: Option, + include_thread_ids: Option, + include_thread_names: Option, + #[serde( + default, + deserialize_with = "parse_mode", + serialize_with = "write_mode" + )] + mode: Option, + #[serde( + default, + deserialize_with = "parse_target", + serialize_with = "write_target" + )] + target: Option, +} + +impl Arbitrary for LoggingConfig { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + let level_strat = proptest::prop_oneof![ + Just(None), + Just(Some(Level::TRACE)), + Just(Some(Level::DEBUG)), + Just(Some(Level::INFO)), + Just(Some(Level::WARN)), + Just(Some(Level::ERROR)), + ]; + + let mode_strat = proptest::prop_oneof![ + Just(None), + Just(Some(LogMode::Compact)), + Just(Some(LogMode::Pretty)), + Just(Some(LogMode::Json)), + ]; + + let target_strat = proptest::prop_oneof![ + Just(None), + Just(Some(LogTarget::StdErr)), + Just(Some(LogTarget::StdOut)), + Just(Some({ + let tempfile = tempfile::NamedTempFile::new().unwrap(); + let name = tempfile.into_temp_path(); + LogTarget::File(name.to_path_buf()) + })), + ]; + + ( + level_strat, + any::>(), + any::>(), + any::>(), + any::>(), + mode_strat, + target_strat, + ) + .prop_map( + |( + level, + include_filename, + include_lineno, + include_thread_ids, + include_thread_names, + mode, + target, + )| LoggingConfig { + level, + include_filename, + include_lineno, + include_thread_ids, + include_thread_names, + mode, + target, + }, + ) + .boxed() + } +} + +fn parse_level<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + + s.map(|x| match x.to_lowercase().as_str() { + "trace" => Ok(Some(Level::TRACE)), + "debug" => Ok(Some(Level::DEBUG)), + "info" => Ok(Some(Level::INFO)), + "warn" => Ok(Some(Level::WARN)), + "error" => Ok(Some(Level::ERROR)), + _ => Err(de::Error::invalid_value( + Unexpected::Str(&x), + &"valid logging level (trace, debug, info, warn, or error", + )), + }) + .unwrap_or_else(|| Ok(None)) +} + +fn write_level(item: &Option, serializer: S) -> Result { + match item { + None => serializer.serialize_none(), + Some(Level::TRACE) => serializer.serialize_some("trace"), + Some(Level::DEBUG) => serializer.serialize_some("debug"), + Some(Level::INFO) => serializer.serialize_some("info"), + Some(Level::WARN) => serializer.serialize_some("warn"), + Some(Level::ERROR) => serializer.serialize_some("error"), + } +} + +fn parse_mode<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + + s.map(|x| match x.to_lowercase().as_str() { + "compact" => Ok(Some(LogMode::Compact)), + "pretty" => Ok(Some(LogMode::Pretty)), + "json" => Ok(Some(LogMode::Json)), + _ => Err(de::Error::invalid_value( + Unexpected::Str(&x), + &"valid logging level (trace, debug, info, warn, or error", + )), + }) + .unwrap_or_else(|| Ok(None)) +} + +fn write_mode(item: &Option, serializer: S) -> Result { + match item { + None => serializer.serialize_none(), + Some(LogMode::Compact) => serializer.serialize_some("compact"), + Some(LogMode::Pretty) => serializer.serialize_some("pretty"), + Some(LogMode::Json) => serializer.serialize_some("json"), + } +} + +fn parse_target<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + + Ok(s.map(|x| match x.to_lowercase().as_str() { + "stdout" => LogTarget::StdOut, + "stderr" => LogTarget::StdErr, + _ => LogTarget::File(x.into()), + })) +} + +fn write_target(item: &Option, serializer: S) -> Result { + match item { + None => serializer.serialize_none(), + Some(LogTarget::StdOut) => serializer.serialize_some("stdout"), + Some(LogTarget::StdErr) => serializer.serialize_some("stderr"), + Some(LogTarget::File(file)) => serializer.serialize_some(file), + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub struct KeyConfig { + pub public: PathBuf, + pub private: PathBuf, + pub password: Option, +} + +impl Arbitrary for KeyConfig { + type Parameters = bool; + type Strategy = BoxedStrategy; + + fn arbitrary_with(_generate_real_keys: Self::Parameters) -> Self::Strategy { + let password = proptest::string::string_regex("[a-zA-Z0-9_!@#$%^&*]{8,40}").unwrap(); + + proptest::option::of(password) + .prop_map(|password| { + let public_file = tempfile::NamedTempFile::new().unwrap(); + let private_file = tempfile::NamedTempFile::new().unwrap(); + + let public = public_file.into_temp_path().to_path_buf(); + let private = private_file.into_temp_path().to_path_buf(); + + KeyConfig { + public, + private, + password, + } + }) + .boxed() + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub struct ServerConfig { + key_exchange_algorithms: Option>, + server_host_algorithms: Option>, + encryption_algorithms: Option>, + mac_algorithms: Option>, + compression_algorithms: Option>, + predict: Option, +} + +impl Arbitrary for ServerConfig { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + let keyx = proptest::sample::select(ALLOWED_KEY_EXCHANGE_ALGORITHMS); + let hostkey = proptest::sample::select(ALLOWED_HOST_KEY_ALGORITHMS); + let enc = proptest::sample::select(ALLOWED_ENCRYPTION_ALGORITHMS); + let mac = proptest::sample::select(ALLOWED_MAC_ALGORITHMS); + let comp = proptest::sample::select(ALLOWED_COMPRESSION_ALGORITHMS); + + ( + proptest::collection::hash_set(keyx.clone(), 0..ALLOWED_KEY_EXCHANGE_ALGORITHMS.len()), + proptest::collection::hash_set(hostkey, 0..ALLOWED_HOST_KEY_ALGORITHMS.len()), + proptest::collection::hash_set(enc, 0..ALLOWED_ENCRYPTION_ALGORITHMS.len()), + proptest::collection::hash_set(mac, 0..ALLOWED_MAC_ALGORITHMS.len()), + proptest::collection::hash_set(comp, 0..ALLOWED_COMPRESSION_ALGORITHMS.len()), + proptest::option::of(keyx), + ) + .prop_map(|(kex, host, enc, mac, comp, pred)| ServerConfig { + key_exchange_algorithms: finalize_options(kex), + server_host_algorithms: finalize_options(host), + encryption_algorithms: finalize_options(enc), + mac_algorithms: finalize_options(mac), + compression_algorithms: finalize_options(comp), + predict: pred.map(str::to_string), + }) + .boxed() + } +} + +fn finalize_options(values: HashSet<&str>) -> Option> { + if values.is_empty() { + None + } else { + Some(values.into_iter().map(str::to_string).collect()) + } +} + +#[derive(Debug, Error)] +pub enum ConfigFileError { + #[error("Could not open provided config file: {0}")] + CouldNotOpen(std::io::Error), + #[error("Could not read config file: {0}")] + CouldNotRead(std::io::Error), + #[error("Error in config file: {0}")] + ParseError(#[from] toml::de::Error), +} + +impl ConfigFile { + /// Try to read in a config file, using the given path if provided, or the XDG-standard + /// path if not. + /// + /// If the user didn't provide a config file, and there isn't a config file in the standard + /// XDG place, returns `Ok(None)`. + /// + /// This will return errors if there is a parse error in understanding the file, if + /// there's some basic disk error in reading the file, or if the user provided a config + /// file that we couldn't find on disk. + pub fn new(provided_path: Option) -> Result, ConfigFileError> { + let config_file = if let Some(path) = provided_path { + let file = std::fs::File::open(path).map_err(ConfigFileError::CouldNotOpen)?; + Some(file) + } else { + let Ok(xdg_base_dirs) = xdg::BaseDirectories::with_prefix("hushd") else { + return Ok(None); + }; + let path = xdg_base_dirs.find_config_file("config.toml"); + path.and_then(|x| std::fs::File::open(x).ok()) + }; + + let Some(mut config_file) = config_file else { + return Ok(None); + }; + + let mut contents = String::new(); + let _ = config_file + .read_to_string(&mut contents) + .map_err(ConfigFileError::CouldNotRead)?; + let config = toml::from_str(&contents)?; + + Ok(Some(config)) + } + + /// Merge any settings found in the config file into our current configuration. + /// + pub fn merge_standard_options_into( + &mut self, + runtime: &mut RuntimeConfiguration, + _logging: &mut LoggingConfiguration, + ) { + if let Some(runtime_config) = self.runtime.take() { + runtime.tokio_worker_threads = runtime_config + .worker_threads + .unwrap_or(runtime.tokio_worker_threads); + runtime.tokio_blocking_threads = runtime_config + .blocking_threads + .unwrap_or(runtime.tokio_blocking_threads); + } + } +} + +#[test] +fn all_keys_example_parses() { + let path = format!("{}/tests/all_keys.toml", env!("CARGO_MANIFEST_DIR")); + let result = ConfigFile::new(Some(path.into())); + assert!(result.is_ok()); +} + +proptest::proptest! { + #[test] + fn valid_configs_parse(config in ConfigFile::arbitrary()) { + use std::io::Write; + + let mut tempfile = tempfile::NamedTempFile::new().unwrap(); + let contents = toml::to_string(&config).unwrap(); + tempfile.write_all(contents.as_bytes()).unwrap(); + let path = tempfile.into_temp_path(); + + let parsed = ConfigFile::new(Some(path.to_path_buf())).unwrap().unwrap(); + assert_eq!(config, parsed); + } +} diff --git a/src/config/console.rs b/src/config/console.rs new file mode 100644 index 0000000..c96dc8f --- /dev/null +++ b/src/config/console.rs @@ -0,0 +1,48 @@ +use console_subscriber::{ConsoleLayer, ServerAddr}; +use core::time::Duration; +use std::path::PathBuf; +use tracing_subscriber::Layer; + +pub struct ConsoleConfiguration { + client_buffer_capacity: usize, + publish_interval: Duration, + retention: Duration, + pub server_addr: ServerAddr, + poll_duration_histogram_max: Duration, +} + +impl Default for ConsoleConfiguration { + fn default() -> Self { + ConsoleConfiguration { + client_buffer_capacity: ConsoleLayer::DEFAULT_CLIENT_BUFFER_CAPACITY, + publish_interval: ConsoleLayer::DEFAULT_PUBLISH_INTERVAL, + retention: ConsoleLayer::DEFAULT_RETENTION, + server_addr: xdg::BaseDirectories::with_prefix("hushd") + .and_then(|x| x.get_runtime_directory().cloned()) + .map(|mut v| { + v.push("console.sock"); + v + }) + .map(|p| ServerAddr::Unix(p.clone())) + .unwrap_or_else(|_| PathBuf::from("console.sock").into()), + poll_duration_histogram_max: ConsoleLayer::DEFAULT_SCHEDULED_DURATION_MAX, + } + } +} + +impl ConsoleConfiguration { + #[cfg(not(tarpaulin_include))] + pub fn layer(&self) -> Box + Send + Sync + 'static> + where + S: tracing_core::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, + { + ConsoleLayer::builder() + .client_buffer_capacity(self.client_buffer_capacity) + .publish_interval(self.publish_interval) + .retention(self.retention) + .server_addr(self.server_addr.clone()) + .poll_duration_histogram_max(self.poll_duration_histogram_max) + .spawn() + .boxed() + } +} diff --git a/src/config/error.rs b/src/config/error.rs new file mode 100644 index 0000000..51b04ce --- /dev/null +++ b/src/config/error.rs @@ -0,0 +1,22 @@ +use crate::config::config_file::ConfigFileError; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ConfigurationError { + #[error(transparent)] + ConfigFile(#[from] ConfigFileError), + #[error(transparent)] + CommandLineError(#[from] clap::error::Error), + #[error("Could not read file {file}: {error}")] + CouldNotRead { + file: PathBuf, + error: std::io::Error, + }, + #[error("Error loading public key information")] + PublicKey, + #[error("Error loading private key")] + PrivateKey, + #[error("Error configuring DNS resolver")] + Resolver, +} diff --git a/src/config/logging.rs b/src/config/logging.rs new file mode 100644 index 0000000..8cde884 --- /dev/null +++ b/src/config/logging.rs @@ -0,0 +1,95 @@ +use std::path::PathBuf; +use tracing_core::Subscriber; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::{EnvFilter, Layer}; + +pub struct LoggingConfiguration { + pub(crate) filter: LevelFilter, + pub(crate) include_filename: bool, + pub(crate) include_lineno: bool, + pub(crate) include_thread_ids: bool, + pub(crate) include_thread_names: bool, + pub(crate) mode: LogMode, + pub(crate) target: LogTarget, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LogMode { + Compact, + Pretty, + Json, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum LogTarget { + StdOut, + StdErr, + File(PathBuf), +} + +impl LogTarget { + fn supports_ansi(&self) -> bool { + matches!(self, LogTarget::StdOut | LogTarget::StdErr) + } +} + +impl Default for LoggingConfiguration { + fn default() -> Self { + LoggingConfiguration { + filter: LevelFilter::INFO, + include_filename: false, + include_lineno: false, + include_thread_ids: true, + include_thread_names: true, + mode: LogMode::Compact, + target: LogTarget::StdErr, + } + } +} + +impl LoggingConfiguration { + #[cfg(not(tarpaulin_include))] + pub fn layer(&self) -> Result + Send + Sync + 'static>, std::io::Error> + where + S: Subscriber + for<'a> LookupSpan<'a>, + { + let filter = EnvFilter::builder() + .with_default_directive(self.filter.into()) + .with_env_var("HUSHD_LOG") + .from_env_lossy(); + + let base = tracing_subscriber::fmt::layer() + .with_file(self.include_filename) + .with_line_number(self.include_lineno) + .with_thread_ids(self.include_thread_ids) + .with_thread_names(self.include_thread_names) + .with_ansi(self.target.supports_ansi()); + + macro_rules! finalize { + ($layer: expr) => { + match self.mode { + LogMode::Compact => Ok($layer.compact().with_filter(filter).boxed()), + LogMode::Json => Ok($layer.json().with_filter(filter).boxed()), + LogMode::Pretty => Ok($layer.pretty().with_filter(filter).boxed()), + } + }; + } + + match self.target { + LogTarget::StdOut => finalize!(base.with_writer(std::io::stdout)), + LogTarget::StdErr => finalize!(base.with_writer(std::io::stderr)), + LogTarget::File(ref path) => { + let log_file = std::fs::File::create(path)?; + finalize!(base.with_writer(std::sync::Mutex::new(log_file))) + } + } + } +} + +#[test] +fn supports_ansi() { + assert!(LogTarget::StdOut.supports_ansi()); + assert!(LogTarget::StdErr.supports_ansi()); + assert!(!LogTarget::File("/dev/null".into()).supports_ansi()); +} diff --git a/src/config/resolver.rs b/src/config/resolver.rs new file mode 100644 index 0000000..3d23c4e --- /dev/null +++ b/src/config/resolver.rs @@ -0,0 +1,304 @@ +use error_stack::report; +use hickory_proto::error::ProtoError; +use hickory_resolver::config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts}; +use hickory_resolver::{Name, TokioAsyncResolver}; +use proptest::arbitrary::Arbitrary; +use proptest::strategy::{BoxedStrategy, Just, Strategy}; +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; +use thiserror::Error; + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +pub struct DnsConfig { + built_in: Option, + local_domain: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + search_domains: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + name_servers: Vec, + #[serde(default)] + timeout_in_seconds: Option, + #[serde(default)] + retry_attempts: Option, + #[serde(default)] + cache_size: Option, + #[serde(default)] + use_hosts_file: Option, + #[serde(default)] + max_concurrent_requests_for_query: Option, + #[serde(default)] + preserve_intermediates: Option, + #[serde(default)] + shuffle_dns_servers: Option, +} + +impl Default for DnsConfig { + fn default() -> Self { + DnsConfig { + built_in: Some(BuiltinDnsOption::Cloudflare), + local_domain: None, + search_domains: vec![], + name_servers: vec![], + timeout_in_seconds: None, + retry_attempts: None, + cache_size: None, + use_hosts_file: None, + max_concurrent_requests_for_query: None, + preserve_intermediates: None, + shuffle_dns_servers: None, + } + } +} + +impl Arbitrary for DnsConfig { + type Parameters = bool; + type Strategy = BoxedStrategy; + + fn arbitrary_with(always_use_builtin: Self::Parameters) -> Self::Strategy { + if always_use_builtin { + BuiltinDnsOption::arbitrary() + .prop_map(|x| DnsConfig { + built_in: Some(x), + local_domain: None, + search_domains: vec![], + name_servers: vec![], + timeout_in_seconds: None, + retry_attempts: None, + cache_size: None, + use_hosts_file: None, + max_concurrent_requests_for_query: None, + preserve_intermediates: None, + shuffle_dns_servers: None, + }) + .boxed() + } else { + let built_in = proptest::option::of(BuiltinDnsOption::arbitrary()); + built_in + .prop_flat_map(|built_in| { + let local_domain = proptest::option::of(domain_name_strat()); + let search_domains = proptest::collection::vec(domain_name_strat(), 0..10); + let min_servers = if built_in.is_some() { 0 } else { 1 }; + let name_servers = + proptest::collection::vec(ServerConfig::arbitrary(), min_servers..6); + let timeout_in_seconds = proptest::option::of(u16::arbitrary()); + let retry_attempts = proptest::option::of(u16::arbitrary()); + let cache_size = proptest::option::of(u32::arbitrary()); + let use_hosts_file = proptest::option::of(bool::arbitrary()); + let max_concurrent_requests_for_query = proptest::option::of(u16::arbitrary()); + let preserve_intermediates = proptest::option::of(bool::arbitrary()); + let shuffle_dns_servers = proptest::option::of(bool::arbitrary()); + + ( + local_domain, + search_domains, + name_servers, + timeout_in_seconds, + retry_attempts, + cache_size, + use_hosts_file, + max_concurrent_requests_for_query, + preserve_intermediates, + shuffle_dns_servers, + ) + .prop_map( + move |( + local_domain, + search_domains, + name_servers, + timeout_in_seconds, + retry_attempts, + cache_size, + use_hosts_file, + max_concurrent_requests_for_query, + preserve_intermediates, + shuffle_dns_servers, + )| DnsConfig { + built_in, + local_domain, + search_domains, + name_servers, + timeout_in_seconds, + retry_attempts, + cache_size, + use_hosts_file, + max_concurrent_requests_for_query, + preserve_intermediates, + shuffle_dns_servers, + }, + ) + }) + .boxed() + } + } +} + +fn domain_name_strat() -> BoxedStrategy { + let chunk = proptest::string::string_regex("[a-zA-Z0-9]{2,32}").unwrap(); + let sets = proptest::collection::vec(chunk, 2..6); + sets.prop_map(|set| { + let mut output = String::new(); + + for x in set.into_iter() { + if !output.is_empty() { + output.push('.'); + } + + output.push_str(&x); + } + + output + }) + .boxed() +} + +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +enum BuiltinDnsOption { + Google, + Cloudflare, + Quad9, +} + +impl Arbitrary for BuiltinDnsOption { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + proptest::prop_oneof![ + Just(BuiltinDnsOption::Google), + Just(BuiltinDnsOption::Cloudflare), + Just(BuiltinDnsOption::Quad9), + ] + .boxed() + } +} + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +struct ServerConfig { + address: SocketAddr, + #[serde(default)] + trust_negatives: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + bind_address: Option, +} + +impl Arbitrary for ServerConfig { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + ( + SocketAddr::arbitrary(), + bool::arbitrary(), + proptest::option::of(SocketAddr::arbitrary()), + ) + .prop_map(|(mut address, trust_negatives, mut bind_address)| { + clear_flow_and_scope_info(&mut address); + if let Some(bind_address) = bind_address.as_mut() { + clear_flow_and_scope_info(bind_address); + } + + ServerConfig { + address, + trust_negatives, + bind_address, + } + }) + .boxed() + } +} + +fn clear_flow_and_scope_info(address: &mut SocketAddr) { + if let SocketAddr::V6(addr) = address { + addr.set_flowinfo(0); + addr.set_scope_id(0); + } +} + +#[derive(Debug, Error)] +pub enum ResolverConfigError { + #[error("Bad local domain name '{name}' provided: {error}")] + BadDomainName { name: String, error: ProtoError }, + #[error("Bad domain name for search '{name}' provided: {error}")] + BadSearchName { name: String, error: ProtoError }, + #[error("No DNS hosts found to search")] + NoHosts, +} + +impl DnsConfig { + /// Convert this resolver configuration into an actual ResolverConfig, or say + /// why it's bad. + pub fn resolver(&self) -> error_stack::Result { + let mut config = match &self.built_in { + None => ResolverConfig::new(), + Some(BuiltinDnsOption::Cloudflare) => ResolverConfig::cloudflare(), + Some(BuiltinDnsOption::Google) => ResolverConfig::google(), + Some(BuiltinDnsOption::Quad9) => ResolverConfig::quad9(), + }; + + if let Some(local_domain) = &self.local_domain { + let name = Name::from_utf8(local_domain).map_err(|error| { + report!(ResolverConfigError::BadDomainName { + name: local_domain.clone(), + error, + }) + })?; + config.set_domain(name); + } + + for name in self.search_domains.iter() { + let name = Name::from_utf8(name).map_err(|error| { + report!(ResolverConfigError::BadSearchName { + name: name.clone(), + error, + }) + })?; + config.add_search(name); + } + + for ns in self.name_servers.iter() { + let mut nsconfig = NameServerConfig::new(ns.address, Protocol::Udp); + + nsconfig.trust_negative_responses = ns.trust_negatives; + nsconfig.bind_addr = ns.bind_address; + + config.add_name_server(nsconfig); + } + + if config.name_servers().is_empty() { + return Err(report!(ResolverConfigError::NoHosts)); + } + + let mut options = ResolverOpts::default(); + + if let Some(seconds) = self.timeout_in_seconds { + options.timeout = tokio::time::Duration::from_secs(seconds as u64); + } + if let Some(retries) = self.retry_attempts { + options.attempts = retries as usize; + } + if let Some(cache_size) = self.cache_size { + options.cache_size = cache_size as usize; + } + options.use_hosts_file = self.use_hosts_file.unwrap_or(true); + if let Some(max) = self.max_concurrent_requests_for_query { + options.num_concurrent_reqs = max as usize; + } + if let Some(preserve) = self.preserve_intermediates { + options.preserve_intermediates = preserve; + } + if let Some(shuffle) = self.shuffle_dns_servers { + options.shuffle_dns_servers = shuffle; + } + + Ok(TokioAsyncResolver::tokio(config, options)) + } +} + +proptest::proptest! { + #[test] + fn valid_configs_parse(config in DnsConfig::arbitrary_with(false)) { + let toml = toml::to_string(&config).unwrap(); + let reversed: DnsConfig = toml::from_str(&toml).unwrap(); + assert_eq!(config, reversed); + } +} diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..1b940a9 --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,272 @@ +pub mod known_algorithms; +pub mod rsa; + +use std::fmt; +use std::str::FromStr; +use thiserror::Error; + +#[derive(Debug, PartialEq, Eq)] +pub enum KeyExchangeAlgorithm { + EcdhSha2Nistp256, + EcdhSha2Nistp384, + EcdhSha2Nistp521, + Curve25519Sha256, +} + +impl KeyExchangeAlgorithm { + pub fn allowed() -> &'static [KeyExchangeAlgorithm] { + &[ + KeyExchangeAlgorithm::EcdhSha2Nistp256, + KeyExchangeAlgorithm::EcdhSha2Nistp384, + KeyExchangeAlgorithm::EcdhSha2Nistp521, + KeyExchangeAlgorithm::Curve25519Sha256, + ] + } +} + +#[derive(Debug, Error)] +pub enum AlgoFromStrError { + #[error("Did not recognize key exchange algorithm '{0}'")] + UnknownKeyExchangeAlgorithm(String), + #[error("Did not recognize host key algorithm '{0}'")] + UnknownHostKeyAlgorithm(String), + #[error("Did not recognize encryption algorithm '{0}'")] + UnknownEncryptionAlgorithm(String), + #[error("Did not recognize MAC algorithm '{0}'")] + UnknownMacAlgorithm(String), + #[error("Did not recognize compression algorithm '{0}'")] + UnknownCompressionAlgorithm(String), +} + +impl FromStr for KeyExchangeAlgorithm { + type Err = AlgoFromStrError; + + fn from_str(s: &str) -> Result { + match s { + "ecdh-sha2-nistp256" => Ok(KeyExchangeAlgorithm::EcdhSha2Nistp256), + "ecdh-sha2-nistp384" => Ok(KeyExchangeAlgorithm::EcdhSha2Nistp384), + "ecdh-sha2-nistp521" => Ok(KeyExchangeAlgorithm::EcdhSha2Nistp521), + "curve25519-sha256" => Ok(KeyExchangeAlgorithm::Curve25519Sha256), + other => Err(AlgoFromStrError::UnknownKeyExchangeAlgorithm( + other.to_string(), + )), + } + } +} + +impl fmt::Display for KeyExchangeAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + KeyExchangeAlgorithm::EcdhSha2Nistp256 => write!(f, "ecdh-sha2-nistp256"), + KeyExchangeAlgorithm::EcdhSha2Nistp384 => write!(f, "ecdh-sha2-nistp384"), + KeyExchangeAlgorithm::EcdhSha2Nistp521 => write!(f, "ecdh-sha2-nistp521"), + KeyExchangeAlgorithm::Curve25519Sha256 => write!(f, "curve25519-sha256"), + } + } +} + +#[test] +fn can_invert_kex_algos() { + for variant in KeyExchangeAlgorithm::allowed().iter() { + let s = variant.to_string(); + let reversed = KeyExchangeAlgorithm::from_str(&s); + assert!(reversed.is_ok()); + assert_eq!(variant, &reversed.unwrap()); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum HostKeyAlgorithm { + Ed25519, + EcdsaSha2Nistp256, + EcdsaSha2Nistp384, + EcdsaSha2Nistp521, + Rsa, +} + +impl HostKeyAlgorithm { + pub fn allowed() -> &'static [Self] { + &[ + HostKeyAlgorithm::Ed25519, + HostKeyAlgorithm::EcdsaSha2Nistp256, + HostKeyAlgorithm::EcdsaSha2Nistp384, + HostKeyAlgorithm::EcdsaSha2Nistp521, + HostKeyAlgorithm::Rsa, + ] + } +} + +impl fmt::Display for HostKeyAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HostKeyAlgorithm::Ed25519 => write!(f, "ssh-ed25519"), + HostKeyAlgorithm::EcdsaSha2Nistp256 => write!(f, "ecdsa-sha2-nistp256"), + HostKeyAlgorithm::EcdsaSha2Nistp384 => write!(f, "ecdsa-sha2-nistp384"), + HostKeyAlgorithm::EcdsaSha2Nistp521 => write!(f, "ecdsa-sha2-nistp521"), + HostKeyAlgorithm::Rsa => write!(f, "ssh-rsa"), + } + } +} + +impl FromStr for HostKeyAlgorithm { + type Err = AlgoFromStrError; + + fn from_str(s: &str) -> Result { + match s { + "ssh-ed25519" => Ok(HostKeyAlgorithm::Ed25519), + "ecdsa-sha2-nistp256" => Ok(HostKeyAlgorithm::EcdsaSha2Nistp256), + "ecdsa-sha2-nistp384" => Ok(HostKeyAlgorithm::EcdsaSha2Nistp384), + "ecdsa-sha2-nistp521" => Ok(HostKeyAlgorithm::EcdsaSha2Nistp521), + "ssh-rsa" => Ok(HostKeyAlgorithm::Rsa), + unknown => Err(AlgoFromStrError::UnknownHostKeyAlgorithm( + unknown.to_string(), + )), + } + } +} + +#[test] +fn can_invert_host_key_algos() { + for variant in HostKeyAlgorithm::allowed().iter() { + let s = variant.to_string(); + let reversed = HostKeyAlgorithm::from_str(&s); + assert!(reversed.is_ok()); + assert_eq!(variant, &reversed.unwrap()); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum EncryptionAlgorithm { + Aes256Ctr, + Aes256Gcm, + ChaCha20Poly1305, +} + +impl EncryptionAlgorithm { + pub fn allowed() -> &'static [Self] { + &[ + EncryptionAlgorithm::Aes256Ctr, + EncryptionAlgorithm::Aes256Gcm, + EncryptionAlgorithm::ChaCha20Poly1305, + ] + } +} + +impl fmt::Display for EncryptionAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EncryptionAlgorithm::Aes256Ctr => write!(f, "aes256-ctr"), + EncryptionAlgorithm::Aes256Gcm => write!(f, "aes256-gcm@openssh.com"), + EncryptionAlgorithm::ChaCha20Poly1305 => write!(f, "chacha20-poly1305@openssh.com"), + } + } +} + +impl FromStr for EncryptionAlgorithm { + type Err = AlgoFromStrError; + + fn from_str(s: &str) -> Result { + match s { + "aes256-ctr" => Ok(EncryptionAlgorithm::Aes256Ctr), + "aes256-gcm@openssh.com" => Ok(EncryptionAlgorithm::Aes256Gcm), + "chacha20-poly1305@openssh.com" => Ok(EncryptionAlgorithm::ChaCha20Poly1305), + _ => Err(AlgoFromStrError::UnknownEncryptionAlgorithm(s.to_string())), + } + } +} + +#[test] +fn can_invert_encryption_algos() { + for variant in EncryptionAlgorithm::allowed().iter() { + let s = variant.to_string(); + let reversed = EncryptionAlgorithm::from_str(&s); + assert!(reversed.is_ok()); + assert_eq!(variant, &reversed.unwrap()); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum MacAlgorithm { + HmacSha256, + HmacSha512, +} + +impl MacAlgorithm { + pub fn allowed() -> &'static [Self] { + &[MacAlgorithm::HmacSha256, MacAlgorithm::HmacSha512] + } +} + +impl fmt::Display for MacAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MacAlgorithm::HmacSha256 => write!(f, "hmac-sha2-256"), + MacAlgorithm::HmacSha512 => write!(f, "hmac-sha2-512"), + } + } +} + +impl FromStr for MacAlgorithm { + type Err = AlgoFromStrError; + + fn from_str(s: &str) -> Result { + match s { + "hmac-sha2-256" => Ok(MacAlgorithm::HmacSha256), + "hmac-sha2-512" => Ok(MacAlgorithm::HmacSha512), + _ => Err(AlgoFromStrError::UnknownMacAlgorithm(s.to_string())), + } + } +} + +#[test] +fn can_invert_mac_algos() { + for variant in MacAlgorithm::allowed().iter() { + let s = variant.to_string(); + let reversed = MacAlgorithm::from_str(&s); + assert!(reversed.is_ok()); + assert_eq!(variant, &reversed.unwrap()); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum CompressionAlgorithm { + None, + Zlib, +} + +impl CompressionAlgorithm { + pub fn allowed() -> &'static [Self] { + &[CompressionAlgorithm::None, CompressionAlgorithm::Zlib] + } +} + +impl fmt::Display for CompressionAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CompressionAlgorithm::None => write!(f, "none"), + CompressionAlgorithm::Zlib => write!(f, "zlib"), + } + } +} + +impl FromStr for CompressionAlgorithm { + type Err = AlgoFromStrError; + + fn from_str(s: &str) -> Result { + match s { + "none" => Ok(CompressionAlgorithm::None), + "zlib" => Ok(CompressionAlgorithm::Zlib), + _ => Err(AlgoFromStrError::UnknownCompressionAlgorithm(s.to_string())), + } + } +} + +#[test] +fn can_invert_compression_algos() { + for variant in CompressionAlgorithm::allowed().iter() { + let s = variant.to_string(); + let reversed = CompressionAlgorithm::from_str(&s); + assert!(reversed.is_ok()); + assert_eq!(variant, &reversed.unwrap()); + } +} diff --git a/src/crypto/known_algorithms.rs b/src/crypto/known_algorithms.rs new file mode 100644 index 0000000..a8d9615 --- /dev/null +++ b/src/crypto/known_algorithms.rs @@ -0,0 +1,24 @@ +pub static ALLOWED_KEY_EXCHANGE_ALGORITHMS: &[&str] = &[ + "ecdh-sha2-nistp256", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp521", + "curve25519-sha256", +]; + +pub static ALLOWED_HOST_KEY_ALGORITHMS: &[&str] = &[ + "ssh-ed25519", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + "ssh-rsa", +]; + +pub static ALLOWED_ENCRYPTION_ALGORITHMS: &[&str] = &[ + "aes256-ctr", + "aes256-gcm@openssh.com", + "chacha20-poly1305@openssh.com", +]; + +pub static ALLOWED_MAC_ALGORITHMS: &[&str] = &["hmac-sha2-256", "hmac-sha2-512"]; + +pub static ALLOWED_COMPRESSION_ALGORITHMS: &[&str] = &["none", "zlib"]; diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs new file mode 100644 index 0000000..89a027a --- /dev/null +++ b/src/crypto/rsa.rs @@ -0,0 +1,251 @@ +use num_bigint_dig::{BigInt, BigUint, ModInverse}; +use num_integer::{sqrt, Integer}; +use num_traits::Pow; +use zeroize::{Zeroize, Zeroizing}; + +/// An RSA public key +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct PublicKey { + /// The public modulus, the product of the primes 'p' and 'q' + n: BigUint, + /// The public exponent. + e: BigUint, +} + +/// An RSA private key +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct PrivateKey { + /// The public modulus, the product of the primes 'p' and 'q' + n: BigUint, + /// The public exponent + e: BigUint, + /// The private exponent + d: BigUint, + /// The prime 'p' + p: BigUint, + /// The prime 'q' + q: BigUint, + /// d mod (p-1) + dmodp1: BigUint, + /// d mod (q-1) + dmodq1: BigUint, + /// q^-1 mod P + qinv: BigInt, +} + +impl Drop for PrivateKey { + fn drop(&mut self) { + self.d.zeroize(); + self.p.zeroize(); + self.q.zeroize(); + self.dmodp1.zeroize(); + self.dmodq1.zeroize(); + self.qinv.zeroize(); + } +} + +impl PublicKey { + /// Generate a public key from the given input values. + /// + /// No checking is performed at this point to ensure that these + /// values are sane in any way. + pub fn new(n: BigUint, e: BigUint) -> Self { + PublicKey { n, e } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum PrivateKeyLoadError { + #[error("Invalid value for public 'e' value; must be between 2^16 and 2^256, got {0}")] + InvalidEValue(BigUint), + #[error("Could not recover primes 'p' and 'q' from provided private key data")] + CouldNotRecoverPrimes, + #[error("Could not generate modular inverse of 'q' in provided private key data")] + CouldNotGenerateModInv, + #[error("Could not cross-confirm value '{value}' in provided private key data")] + IncoherentValue { value: &'static str }, +} + +impl PrivateKey { + /// Generate a private key from the associated public key and the private + /// value 'd'. + /// + /// This will do some further computations, and should not be called when + /// you absolutely must get an answer back immediately. Or, to put it + /// another way, you really should call this with `block_in_place` or + /// similar in an async context. + pub fn from_d(public: &PublicKey, d: BigUint) -> Result { + let (p, q) = recover_primes(&public.n, &public.e, &d)?; + let dmodp1 = &d % (&p - 1u64); + let dmodq1 = &d % (&q - 1u64); + let qinv = (&q) + .mod_inverse(&p) + .ok_or(PrivateKeyLoadError::CouldNotGenerateModInv)?; + + Ok(PrivateKey { + n: public.n.clone(), + e: public.e.clone(), + d, + p, + q, + dmodp1, + dmodq1, + qinv, + }) + } + + /// Generate a private key from the associated public key, the private + /// value 'd', and several other useful values. + /// + /// This will do some additional computations, and should not be called + /// when you absolutely must get an answer back immediately. Or, to put + /// it another way, you really should call this with `block_in_place` or + /// similar. + /// + /// This version of this function performs some safety checking to ensure + /// the provided values are reasonable. To avoid this cost, but at the + /// risk of accepting bad key data, use the `_unchecked` variant. + pub fn from_parts( + public: &PublicKey, + d: BigUint, + qinv: BigInt, + p: BigUint, + q: BigUint, + ) -> Result { + let computed_private = Self::from_d(public, d)?; + + if qinv != computed_private.qinv { + return Err(PrivateKeyLoadError::IncoherentValue { value: "qinv" }); + } + + if p != computed_private.p { + return Err(PrivateKeyLoadError::IncoherentValue { value: "p" }); + } + + if q != computed_private.q { + return Err(PrivateKeyLoadError::IncoherentValue { value: "q" }); + } + + Ok(computed_private) + } + + /// Generate a private key from the associated public key, the private + /// value 'd', and several other useful values. + /// + /// This will do some additional computations, and should not be called + /// when you absolutely must get an answer back immediately. Or, to put + /// it another way, you really should call this with `block_in_place` or + /// similar. + /// + /// This version of this function performs no safety checking to ensure + /// the provided values are reasonable. + pub fn from_parts_unchecked( + public: &PublicKey, + d: BigUint, + qinv: BigInt, + p: BigUint, + q: BigUint, + ) -> Result { + let dmodp1 = &d % (&p - 1u64); + let dmodq1 = &d % (&q - 1u64); + + Ok(PrivateKey { + n: public.n.clone(), + e: public.e.clone(), + d, + qinv, + p, + q, + dmodp1, + dmodq1, + }) + } +} + +/// Recover the two primes, `p` and `q`, used to generate the given private +/// key. +/// +/// This algorithm is as straightforward an implementation of Appendix C.1 +/// of NIST 800-56b, revision 2, as I could make it. +fn recover_primes( + n: &BigUint, + e: &BigUint, + d: &BigUint, +) -> Result<(BigUint, BigUint), PrivateKeyLoadError> { + // Assumptions: + // 1. The modulus n is the product of two prime factors p and q, with p > q. + // 2. Both p and q are less than 2^(nBits/2), where nBits ≥ 2048 is the bit length of n. + let n_bits = n.bits() * 8; + let max_p_or_q = BigUint::from(2u8).pow(n_bits / 2); + // 3. The public exponent e is an odd integer between 2^16 and 2^256. + let two = BigUint::from(2u64); + if e < &two.pow(16u64) || e > &two.pow(256u64) { + return Err(PrivateKeyLoadError::InvalidEValue(e.clone())); + } + // 4. The private exponent d is a positive integer that is less than λ(n) = LCM(p – 1, q – 1). + // 5. The exponents e and d satisfy de ≡ 1 (mod λ(n)). + + // Implementation: + // 1. Let a = (de – 1) × GCD(n – 1, de – 1). + let mut de_minus_1 = Zeroizing::new(d * e); + *de_minus_1 -= 1u64; + let n_minus_one = Zeroizing::new(n - 1u64); + let gcd_of_n1_and_de1 = Zeroizing::new(n_minus_one.gcd(&de_minus_1)); + let a = Zeroizing::new(&*de_minus_1 * &*gcd_of_n1_and_de1); + // 2. Let m = a/n and r = a – mn, so that a = mn + r and 0 ≤ r < n. + let m = Zeroizing::new(&*a / n); + let mn = Zeroizing::new(&*m * n); + if *mn > *a { + // if mn is greater than 'a', then 'r' is going to be negative, which + // violates our assumptions. + return Err(PrivateKeyLoadError::CouldNotRecoverPrimes); + } + let r = Zeroizing::new(&*a - (&*m * n)); + if &*r >= n { + // this violates the other side condition of 2 + return Err(PrivateKeyLoadError::CouldNotRecoverPrimes); + } + // 3. Let b = ( (n – r)/(m + 1) ) + 1; if b is not an integer or b^2 ≤ 4n, then output an + // error indicator, and exit without further processing. + let b = Zeroizing::new(((n - &*r) / (&*m + 1u64)) + 1u64); + let b_squared = Zeroizing::new(&*b * &*b); + // 4n contains no secret information, actually, so no need to add the zeorize trait + let four_n = 4usize * n; + if *b_squared <= four_n { + return Err(PrivateKeyLoadError::CouldNotRecoverPrimes); + } + // 4. Let ϒ be the positive square root of b2 – 4n; if ϒ is not an integer, then output + // an error indicator, and exit without further processing. + let b_squared_minus_four_n = Zeroizing::new(&*b_squared - four_n); + let y = Zeroizing::new(sqrt((*b_squared_minus_four_n).clone())); + let cross_check = Zeroizing::new(&*y * &*y); + if cross_check != b_squared_minus_four_n { + return Err(PrivateKeyLoadError::CouldNotRecoverPrimes); + } + // 5. Let p = (b + ϒ)/2 and let q = (b – ϒ)/2. + let mut p = &*b + &*y; + p >>= 1; + let mut q = &*b - &*y; + q >>= 1; + // go back and check some of our assumptions from above: + // 1. The modulus n is the product of two prime factors p and q, with p > q. + if n != &(&p * &q) { + p.zeroize(); + q.zeroize(); + return Err(PrivateKeyLoadError::CouldNotRecoverPrimes); + } + if p <= q { + p.zeroize(); + q.zeroize(); + return Err(PrivateKeyLoadError::CouldNotRecoverPrimes); + } + // 2. Both p and q are less than 2^(nBits/2), where nBits ≥ 2048 is the bit length of n. + if p >= max_p_or_q || q >= max_p_or_q { + p.zeroize(); + q.zeroize(); + return Err(PrivateKeyLoadError::CouldNotRecoverPrimes); + } + + // 6. Output (p, q) as the prime factors. + Ok((p, q)) +} diff --git a/src/encodings.rs b/src/encodings.rs new file mode 100644 index 0000000..aa086fd --- /dev/null +++ b/src/encodings.rs @@ -0,0 +1 @@ +pub mod ssh; diff --git a/src/encodings/ssh.rs b/src/encodings/ssh.rs new file mode 100644 index 0000000..6b449f9 --- /dev/null +++ b/src/encodings/ssh.rs @@ -0,0 +1,10 @@ +pub mod buffer; +mod private_key; +mod private_key_file; +mod public_key; +mod public_key_file; + +pub use self::private_key::PrivateKey; +pub use self::private_key_file::{load_openssh_file_keys, PrivateKeyLoadError}; +pub use self::public_key::PublicKey; +pub use self::public_key_file::PublicKeyLoadError; diff --git a/src/encodings/ssh/buffer.rs b/src/encodings/ssh/buffer.rs new file mode 100644 index 0000000..27a2811 --- /dev/null +++ b/src/encodings/ssh/buffer.rs @@ -0,0 +1,195 @@ +use bytes::{Buf, Bytes}; +use error_stack::report; +use thiserror::Error; + +/// A read-only buffer formatted according to the SSH standard. +/// +/// Reads in this buffer are destructive, in that they will advance +/// an internal pointer, and thus generally require a mutable +/// reference. +pub struct SshReadBuffer { + buffer: B, +} + +impl From for SshReadBuffer { + fn from(buffer: B) -> Self { + SshReadBuffer { buffer } + } +} + +#[derive(Debug, Error, PartialEq)] +pub enum SshReadError { + #[error("Attempt to read byte off the end of the buffer")] + CouldNotReadU8, + #[error("Not enough data left in SSH buffer to read a u32 ({remaining} bytes left)")] + CouldNotReadU32 { remaining: usize }, + #[error("Not enough data left to read length from SSH buffer ({remaining} bytes left)")] + CouldNotReadLength { remaining: usize }, + #[error("Encountered truncated SSH buffer; needed {target} bytes, but only had {remaining}")] + TruncatedBuffer { target: usize, remaining: usize }, + #[error("Invalid string in SSH buffer: {error}")] + StringFormatting { error: std::string::FromUtf8Error }, +} + +impl SshReadBuffer { + /// Try to read a single byte from the buffer, advancing the pointer. + pub fn get_u8(&mut self) -> error_stack::Result { + if self.buffer.has_remaining() { + Ok(self.buffer.get_u8()) + } else { + Err(report!(SshReadError::CouldNotReadU8)) + } + } + + /// Read a u32 from the buffer, advancing the pointer + pub fn get_u32(&mut self) -> error_stack::Result { + let remaining = self.buffer.remaining(); + + if remaining < 4 { + return Err(report!(SshReadError::CouldNotReadU32 { remaining })); + } + + Ok(self.buffer.get_u32()) + } + + /// Read the next chunk of bytes out of the read buffer. + pub fn get_bytes(&mut self) -> error_stack::Result { + let remaining = self.buffer.remaining(); + + if remaining < 4 { + return Err(report!(SshReadError::CouldNotReadLength { remaining })); + } + + let length = self.buffer.get_u32() as usize; + + if length > (remaining - 4) { + return Err(report!(SshReadError::TruncatedBuffer { + target: length, + remaining: self.buffer.remaining(), + })); + } + + Ok(self.buffer.copy_to_bytes(length)) + } + + /// Read the next string from the read buffer. + pub fn get_string(&mut self) -> error_stack::Result { + let bytes = self.get_bytes()?.to_vec(); + let string = + String::from_utf8(bytes).map_err(|error| SshReadError::StringFormatting { error })?; + + Ok(string) + } + + /// Returns true iff there is still data available for reading in the underlying + /// buffer. + pub fn has_remaining(&self) -> bool { + self.buffer.has_remaining() + } + + /// Returns the number of bytes remaining in the buffer + pub fn remaining(&self) -> usize { + self.buffer.remaining() + } +} + +#[test] +fn empty_gets_error_properly() { + let mut empty: SshReadBuffer = Bytes::new().into(); + + assert_eq!( + &SshReadError::CouldNotReadU32 { remaining: 0 }, + empty.get_u32().unwrap_err().current_context() + ); + assert_eq!( + &SshReadError::CouldNotReadLength { remaining: 0 }, + empty.get_bytes().unwrap_err().current_context() + ); +} + +#[test] +fn short_read_errors_properly() { + let mut short: SshReadBuffer = Bytes::from(vec![0]).into(); + + assert_eq!( + &SshReadError::CouldNotReadU32 { remaining: 1 }, + short.get_u32().unwrap_err().current_context() + ); + assert_eq!( + &SshReadError::CouldNotReadLength { remaining: 1 }, + short.get_bytes().unwrap_err().current_context() + ); +} + +#[test] +fn truncated_read_errors_properly() { + let mut buffer: SshReadBuffer = Bytes::from(vec![0, 0, 0, 5, 2, 3]).into(); + assert_eq!( + &SshReadError::TruncatedBuffer { + target: 5, + remaining: 2 + }, + buffer.get_bytes().unwrap_err().current_context() + ); + + let mut buffer: SshReadBuffer = Bytes::from(vec![0, 0, 0, 5, 2, 3]).into(); + assert_eq!( + &SshReadError::TruncatedBuffer { + target: 5, + remaining: 2 + }, + buffer.get_string().unwrap_err().current_context() + ); +} + +#[test] +fn bad_string_errors_properly() { + let mut buffer: SshReadBuffer = Bytes::from(vec![0, 0, 0, 2, 0xC3, 0x28]).into(); + assert!(matches!( + buffer.get_string().unwrap_err().current_context(), + SshReadError::StringFormatting { .. }, + )); +} + +#[test] +fn normal_reads_work() { + let mut buffer: SshReadBuffer = Bytes::from(vec![0, 0, 0, 2, 2, 3]).into(); + assert_eq!(2, buffer.get_u32().unwrap()); + assert!(buffer.has_remaining()); + + let mut buffer: SshReadBuffer = Bytes::from(vec![0, 0, 0, 5, 1, 2, 3, 4, 5]).into(); + assert_eq!( + Bytes::from(vec![1, 2, 3, 4, 5]), + buffer.get_bytes().unwrap() + ); + assert!(!buffer.has_remaining()); + + let mut buffer: SshReadBuffer = + Bytes::from(vec![0, 0, 0, 5, 0x48, 0x65, 0x6c, 0x6c, 0x6f]).into(); + assert_eq!("Hello".to_string(), buffer.get_bytes().unwrap()); + assert!(!buffer.has_remaining()); +} + +#[test] +fn sequential_reads_work() { + let mut buffer: SshReadBuffer = Bytes::from(vec![ + 0, 1, 0, 5, 0, 0, 0, 2, 2, 3, 0, 0, 0, 3, 0x66, 0x6f, 0x6f, + ]) + .into(); + assert_eq!(65541, buffer.get_u32().unwrap()); + assert_eq!(Bytes::from(vec![2, 3]), buffer.get_bytes().unwrap()); + assert_eq!("foo".to_string(), buffer.get_string().unwrap()); + assert!(!buffer.has_remaining()); +} + +#[test] +fn remaining_works() { + let mut buffer: SshReadBuffer = Bytes::from(vec![ + 0, 0, 0, 3, 0x66, 0x6f, 0x6f, 0, 0, 0, 3, 0x62, 0x61, 0x72, + ]) + .into(); + + assert_eq!("foo".to_string(), buffer.get_string().unwrap()); + assert_eq!(7, buffer.remaining()); + assert_eq!("bar".to_string(), buffer.get_string().unwrap()); +} diff --git a/src/encodings/ssh/private_key.rs b/src/encodings/ssh/private_key.rs new file mode 100644 index 0000000..6dc557d --- /dev/null +++ b/src/encodings/ssh/private_key.rs @@ -0,0 +1,844 @@ +use crate::crypto::rsa; +use crate::encodings::ssh::buffer::SshReadBuffer; +use crate::encodings::ssh::public_key::PublicKey; +use bytes::Buf; +use elliptic_curve::scalar::ScalarPrimitive; +use elliptic_curve::sec1::FromEncodedPoint; +use error_stack::{report, ResultExt}; +use num_bigint_dig::{BigInt, BigUint}; + +pub enum PrivateKey { + Rsa(rsa::PublicKey, rsa::PrivateKey), + P256(p256::PublicKey, p256::SecretKey), + P384(p384::PublicKey, p384::SecretKey), + P521(p521::PublicKey, p521::SecretKey), + Ed25519(ed25519_dalek::VerifyingKey, ed25519_dalek::SigningKey), +} + +impl PrivateKey { + // Get a copy of the public key associated with this private key. + // + // This function does do a clone, so will have a memory impact, if that's + // important to you. + pub fn public(&self) -> PublicKey { + match self { + PrivateKey::Rsa(public, _) => PublicKey::Rsa(public.clone()), + PrivateKey::P256(public, _) => PublicKey::P256(*public), + PrivateKey::P384(public, _) => PublicKey::P384(*public), + PrivateKey::P521(public, _) => PublicKey::P521(*public), + PrivateKey::Ed25519(public, _) => PublicKey::Ed25519(*public), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum PrivateKeyReadError { + #[error("No private key type indicator found in alleged private key bytes.")] + NoKeyType, + #[error("Unknown key type for private key: '{key_type}'")] + UnrecognizedKeyType { key_type: String }, + #[error("Invalid RSA key in private key bytes")] + BadRsaKey, + #[error("Could not find RSA private key constant {constant}")] + CouldNotFindRsaConstant { constant: &'static str }, + #[error("Could not find curve name for private elliptic curve key")] + CouldNotFindCurveName, + #[error("Encoded curve '{curve}' does not match key type '{key}'")] + MismatchedKeyAndCurve { key: String, curve: String }, + #[error("Could not read public point for {curve} curve")] + CouldNotReadPublicPoint { curve: String }, + #[error("Could not read private scalar for {curve} curve")] + CouldNotReadPrivateScalar { curve: String }, + #[error("Invalid scalar for {curve} curve")] + InvalidScalar { curve: String }, + #[error("Bad private scalar for {curve} curve: {error}")] + BadPrivateScalar { + curve: String, + error: elliptic_curve::Error, + }, + #[error("INTERNAL ERROR: Got way too far with unknown curve {curve}")] + InternalError { curve: String }, + #[error("Could not read ed25519 key's {part} data")] + CouldNotReadEd25519 { part: &'static str }, + #[error("Invalid ed25519 {kind} key: {error}")] + InvalidEd25519Key { kind: &'static str, error: String }, + #[error("Could not decode public point data in private key for {curve} curve: {error}")] + PointDecodeError { curve: String, error: sec1::Error }, + #[error("Bad point for public key in curve {curve}")] + BadPointForPublicKey { curve: String }, +} + +pub fn read_private_key( + ssh_buffer: &mut SshReadBuffer, +) -> error_stack::Result { + let encoded_key_type = ssh_buffer + .get_string() + .change_context(PrivateKeyReadError::NoKeyType)?; + + match encoded_key_type.as_str() { + "ssh-rsa" => { + let n = ssh_buffer + .get_bytes() + .change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "n" })?; + let e = ssh_buffer + .get_bytes() + .change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "e" })?; + let d = ssh_buffer + .get_bytes() + .change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "d" })?; + let qinv = ssh_buffer.get_bytes().change_context( + PrivateKeyReadError::CouldNotFindRsaConstant { constant: "q⁻¹" }, + )?; + let p = ssh_buffer + .get_bytes() + .change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "p" })?; + let q = ssh_buffer + .get_bytes() + .change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "q" })?; + + let n = BigUint::from_bytes_be(&n); + let e = BigUint::from_bytes_be(&e); + let d = BigUint::from_bytes_be(&d); + let qinv = BigInt::from_bytes_be(num_bigint_dig::Sign::Plus, &qinv); + let p = BigUint::from_bytes_be(&p); + let q = BigUint::from_bytes_be(&q); + + let public_key = rsa::PublicKey::new(n, e); + let private_key = rsa::PrivateKey::from_parts(&public_key, d, qinv, p, q) + .change_context(PrivateKeyReadError::BadRsaKey)?; + + Ok(PrivateKey::Rsa(public_key, private_key)) + } + + "ecdsa-sha2-nistp256" | "ecdsa-sha2-nistp384" | "ecdsa-sha2-nistp521" => { + let curve = ssh_buffer + .get_string() + .change_context(PrivateKeyReadError::CouldNotFindCurveName)?; + let max_scalar_byte_length = match (curve.as_str(), encoded_key_type.as_str()) { + ("nistp256", "ecdsa-sha2-nistp256") => 32, + ("nistp384", "ecdsa-sha2-nistp384") => 48, + ("nistp521", "ecdsa-sha2-nistp521") => 66, + _ => { + return Err(report!(PrivateKeyReadError::MismatchedKeyAndCurve { + key: encoded_key_type, + curve, + })) + } + }; + + let public_key_bytes = ssh_buffer.get_bytes().change_context_lazy(|| { + PrivateKeyReadError::CouldNotReadPublicPoint { + curve: curve.clone(), + } + })?; + let mut scalar_bytes = ssh_buffer.get_bytes().change_context_lazy(|| { + PrivateKeyReadError::CouldNotReadPrivateScalar { + curve: curve.clone(), + } + })?; + + while scalar_bytes.remaining() > max_scalar_byte_length { + let zero = scalar_bytes.get_u8(); + + if zero != 0 { + return Err(report!(PrivateKeyReadError::InvalidScalar { curve })); + } + } + + match curve.as_str() { + "nistp256" => { + let public_point = + p256::EncodedPoint::from_bytes(&public_key_bytes).map_err(|error| { + report!(PrivateKeyReadError::PointDecodeError { + curve: curve.clone(), + error + }) + })?; + + let public = p256::PublicKey::from_encoded_point(&public_point) + .into_option() + .ok_or_else(|| { + report!(PrivateKeyReadError::BadPointForPublicKey { + curve: curve.clone(), + }) + })?; + + let scalar = ScalarPrimitive::from_slice(&scalar_bytes).map_err(|error| { + report!(PrivateKeyReadError::BadPrivateScalar { curve, error }) + })?; + + let private = p256::SecretKey::new(scalar); + Ok(PrivateKey::P256(public, private)) + } + + "nistp384" => { + let public_point = + p384::EncodedPoint::from_bytes(&public_key_bytes).map_err(|error| { + report!(PrivateKeyReadError::PointDecodeError { + curve: curve.clone(), + error + }) + })?; + + let public = p384::PublicKey::from_encoded_point(&public_point) + .into_option() + .ok_or_else(|| { + report!(PrivateKeyReadError::BadPointForPublicKey { + curve: curve.clone(), + }) + })?; + + let scalar = ScalarPrimitive::from_slice(&scalar_bytes).map_err(|error| { + report!(PrivateKeyReadError::BadPrivateScalar { curve, error }) + })?; + + let private = p384::SecretKey::new(scalar); + Ok(PrivateKey::P384(public, private)) + } + + "nistp521" => { + let public_point = + p521::EncodedPoint::from_bytes(&public_key_bytes).map_err(|error| { + report!(PrivateKeyReadError::PointDecodeError { + curve: curve.clone(), + error + }) + })?; + + let public = p521::PublicKey::from_encoded_point(&public_point) + .into_option() + .ok_or_else(|| { + report!(PrivateKeyReadError::BadPointForPublicKey { + curve: curve.clone(), + }) + })?; + + let scalar = ScalarPrimitive::from_slice(&scalar_bytes).map_err(|error| { + report!(PrivateKeyReadError::BadPrivateScalar { curve, error }) + })?; + + let private = p521::SecretKey::new(scalar); + Ok(PrivateKey::P521(public, private)) + } + + _ => Err(report!(PrivateKeyReadError::InternalError { curve })), + } + } + + "ssh-ed25519" => { + let public_bytes = ssh_buffer + .get_bytes() + .change_context(PrivateKeyReadError::CouldNotReadEd25519 { part: "public" })?; + let mut private_bytes = ssh_buffer + .get_bytes() + .change_context(PrivateKeyReadError::CouldNotReadEd25519 { part: "private" })?; + + let public_key = + ed25519_dalek::VerifyingKey::try_from(public_bytes.as_ref()).map_err(|error| { + report!(PrivateKeyReadError::InvalidEd25519Key { + kind: "public", + error: format!("{}", error), + }) + })?; + + if private_bytes.remaining() != 64 { + return Err(report!(PrivateKeyReadError::InvalidEd25519Key { + kind: "private", + error: format!( + "key should be 64 bytes long, saw {}", + private_bytes.remaining() + ), + })); + } + + let mut private_key = [0; 64]; + private_bytes.copy_to_slice(&mut private_key); + + let private_key = + ed25519_dalek::SigningKey::from_keypair_bytes(&private_key).map_err(|error| { + report!(PrivateKeyReadError::InvalidEd25519Key { + kind: "private", + error: format!("final load error: {}", error), + }) + })?; + + Ok(PrivateKey::Ed25519(public_key, private_key)) + } + + _ => Err(report!(PrivateKeyReadError::UnrecognizedKeyType { + key_type: encoded_key_type, + })), + } +} + +#[cfg(test)] +const RSA_TEST_KEY: &[u8] = &[ + 0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2d, 0x72, 0x73, 0x61, 0x00, 0x00, 0x02, 0x01, 0x00, + 0xb7, 0x7e, 0xd2, 0x53, 0xf0, 0x92, 0xac, 0x06, 0x53, 0x07, 0x8f, 0xe9, 0x89, 0xd8, 0x92, 0xd4, + 0x08, 0x7e, 0xdd, 0x6b, 0xa4, 0x67, 0xd8, 0xac, 0x4a, 0x3b, 0x8f, 0xbd, 0x2f, 0x3a, 0x19, 0x46, + 0x7c, 0xa5, 0x7f, 0xc1, 0x01, 0xee, 0xe3, 0xbf, 0x9e, 0xaf, 0xed, 0xc8, 0xbc, 0x8c, 0x30, 0x70, + 0x6f, 0xf1, 0xdd, 0xb9, 0x9b, 0x4c, 0x67, 0x7b, 0x8f, 0x7c, 0xcf, 0x85, 0x6f, 0x28, 0x5f, 0xeb, + 0xe3, 0x0b, 0x7f, 0x82, 0xf5, 0xa4, 0x99, 0xc6, 0xae, 0x1c, 0xbd, 0xd6, 0xa9, 0x34, 0xc9, 0x05, + 0xfc, 0xdc, 0xe2, 0x84, 0x86, 0x69, 0xc5, 0x6b, 0x0a, 0xf5, 0x17, 0x5f, 0x52, 0xda, 0x4a, 0xdf, + 0xd9, 0x4a, 0xe2, 0x14, 0x0c, 0xba, 0x96, 0x04, 0x4e, 0x25, 0x38, 0xd1, 0x66, 0x75, 0xf2, 0x27, + 0x68, 0x1f, 0x28, 0xce, 0xa5, 0xa3, 0x22, 0x05, 0xf7, 0x9e, 0x38, 0x70, 0xf7, 0x23, 0x65, 0xfe, + 0x4e, 0x77, 0x66, 0x70, 0x16, 0x89, 0xa3, 0xa7, 0x1b, 0xbd, 0x6d, 0x94, 0x85, 0xa1, 0x6b, 0xe8, + 0xf1, 0xb9, 0xb6, 0x7f, 0x4f, 0xb4, 0x53, 0xa7, 0xfe, 0x2d, 0x89, 0x6a, 0x6e, 0x6d, 0x63, 0x85, + 0xe1, 0x00, 0x83, 0x01, 0xb0, 0x00, 0x8a, 0x30, 0xde, 0xdc, 0x2f, 0x30, 0xbc, 0x89, 0x66, 0x2a, + 0x28, 0x59, 0x31, 0xd9, 0x74, 0x9c, 0xf2, 0xf1, 0xd7, 0x53, 0xa9, 0x7b, 0xeb, 0x97, 0xfd, 0x53, + 0x13, 0x66, 0x59, 0x9d, 0x61, 0x4a, 0x72, 0xf4, 0xa9, 0x22, 0xc8, 0xac, 0x0e, 0xd8, 0x0e, 0x4f, + 0x15, 0x59, 0x9b, 0xaa, 0x96, 0xf9, 0xd5, 0x61, 0xd5, 0x04, 0x4c, 0x09, 0x0d, 0x5a, 0x4e, 0x39, + 0xd6, 0xbe, 0x16, 0x8c, 0x36, 0xe1, 0x1d, 0x59, 0x5a, 0xa5, 0x5c, 0x50, 0x6b, 0x6f, 0x6a, 0xed, + 0x63, 0x04, 0xbc, 0x42, 0xec, 0xcb, 0xea, 0x34, 0xfc, 0x75, 0xcc, 0xd1, 0xca, 0x45, 0x66, 0xd0, + 0xc9, 0x14, 0xae, 0x83, 0xd0, 0x7c, 0x0e, 0x06, 0x1d, 0x4f, 0x15, 0x64, 0x53, 0x56, 0xdb, 0xf2, + 0x49, 0x83, 0x03, 0xae, 0xda, 0xa7, 0x29, 0x7c, 0x42, 0xbf, 0x82, 0x07, 0xbc, 0x44, 0x09, 0x15, + 0x32, 0x4d, 0xc0, 0xdf, 0x8a, 0x04, 0x89, 0xd9, 0xd8, 0xdb, 0x05, 0xa5, 0x60, 0x21, 0xed, 0xcb, + 0x54, 0x74, 0x1e, 0x24, 0x06, 0x4d, 0x69, 0x93, 0x72, 0xe8, 0x59, 0xe1, 0x93, 0x1a, 0x6e, 0x48, + 0x16, 0x31, 0x38, 0x10, 0x0e, 0x0b, 0x34, 0xeb, 0x20, 0x86, 0x9c, 0x60, 0x68, 0xaf, 0x30, 0x5e, + 0x7f, 0x26, 0x37, 0xce, 0xd9, 0xc1, 0x47, 0xdf, 0x2d, 0xba, 0x50, 0x96, 0xcf, 0xf8, 0xf5, 0xe8, + 0x65, 0x26, 0x18, 0x4a, 0x88, 0xe0, 0xd8, 0xab, 0x24, 0xde, 0x3f, 0xa9, 0x64, 0x94, 0xe3, 0xaf, + 0x7b, 0x43, 0xaa, 0x72, 0x64, 0x7c, 0xef, 0xdb, 0x30, 0x87, 0x7d, 0x70, 0xd7, 0xbe, 0x0a, 0xca, + 0x79, 0xe6, 0xb8, 0x3e, 0x23, 0x37, 0x17, 0x7d, 0x0c, 0x41, 0x3d, 0xd9, 0x92, 0xd6, 0x8c, 0x95, + 0x8b, 0x63, 0x0b, 0x63, 0x49, 0x98, 0x0f, 0x1f, 0xc1, 0x95, 0x94, 0x6f, 0x22, 0x0e, 0x47, 0x8f, + 0xee, 0x12, 0xb9, 0x8e, 0x28, 0xc2, 0x94, 0xa2, 0xd4, 0x0a, 0x79, 0x69, 0x93, 0x8a, 0x6f, 0xf4, + 0xae, 0xd1, 0x85, 0x11, 0xbb, 0x6c, 0xd5, 0x41, 0x00, 0x71, 0x9b, 0x24, 0xe4, 0x6d, 0x0a, 0x05, + 0x07, 0x4c, 0x28, 0xa6, 0x88, 0x8c, 0xea, 0x74, 0x19, 0x64, 0x26, 0x5a, 0xc8, 0x28, 0xcc, 0xdf, + 0xa8, 0xea, 0xa7, 0xda, 0xec, 0x03, 0xcd, 0xcb, 0xf3, 0xd7, 0x6b, 0xb6, 0x4a, 0xd8, 0x50, 0x44, + 0x91, 0xde, 0xb2, 0x76, 0x6e, 0x85, 0x21, 0x4b, 0x2f, 0x65, 0x57, 0x76, 0xd3, 0xd9, 0xfa, 0xd2, + 0x98, 0xcb, 0x47, 0xaa, 0x33, 0x69, 0x4e, 0x83, 0x75, 0xfe, 0x8e, 0xac, 0x0a, 0xf6, 0xb6, 0xb7, + 0x00, 0x00, 0x00, 0x03, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x54, 0x94, 0xd7, 0xdc, 0xab, + 0x5a, 0xe0, 0x82, 0xb5, 0xc9, 0x19, 0x94, 0x1b, 0xdf, 0x41, 0xa7, 0x0d, 0x17, 0x75, 0x77, 0x05, + 0xbc, 0x7c, 0x8a, 0xc6, 0x58, 0xf8, 0x23, 0xcb, 0x5e, 0x2b, 0x82, 0x6b, 0x38, 0x5a, 0x50, 0x1c, + 0x55, 0x02, 0x94, 0x34, 0x50, 0x81, 0xf9, 0xf2, 0xb7, 0x68, 0x28, 0x9b, 0xe1, 0x50, 0x44, 0x1b, + 0x0a, 0xb7, 0xf4, 0xa3, 0xaa, 0x73, 0x79, 0xdd, 0x48, 0x2e, 0x16, 0xec, 0x7c, 0x43, 0x55, 0x99, + 0x67, 0x3b, 0x1e, 0xf2, 0xe8, 0xfa, 0xb4, 0xb5, 0x20, 0x48, 0xbd, 0x42, 0xd6, 0x8a, 0x6f, 0x6e, + 0x09, 0xd9, 0x5f, 0x43, 0x18, 0xc0, 0xa2, 0x46, 0xed, 0xaa, 0x6f, 0xce, 0x98, 0x8e, 0xe7, 0x91, + 0x0a, 0x7c, 0xd6, 0x15, 0x33, 0x61, 0x22, 0x5c, 0xe9, 0x67, 0x2a, 0xb4, 0xfb, 0x0f, 0xf3, 0x59, + 0x34, 0x7e, 0x1d, 0x64, 0x0b, 0x81, 0x96, 0xc8, 0xc4, 0x7f, 0x62, 0x1e, 0xc7, 0x38, 0xe7, 0xd7, + 0xeb, 0xb0, 0x0c, 0xfa, 0x63, 0x71, 0xdc, 0x71, 0x50, 0x7c, 0x0e, 0x4f, 0x46, 0x3c, 0x92, 0x28, + 0xaa, 0x45, 0x99, 0x7d, 0x37, 0x7e, 0x4d, 0x1a, 0x03, 0xc0, 0x49, 0x58, 0xf2, 0xc4, 0x70, 0x85, + 0xb1, 0x6a, 0x01, 0xa6, 0xe8, 0xb5, 0xb3, 0xf0, 0x64, 0x21, 0x3c, 0xb3, 0x86, 0x91, 0xcc, 0xdb, + 0xcc, 0xf0, 0xcb, 0x7b, 0x66, 0xec, 0x0b, 0xdc, 0x08, 0x1e, 0x54, 0x29, 0xf0, 0x16, 0xc4, 0xcd, + 0xb0, 0xe4, 0x96, 0x54, 0x54, 0x5d, 0x4d, 0xba, 0x35, 0xeb, 0x3a, 0x96, 0xeb, 0xcc, 0x2e, 0x71, + 0x13, 0x4e, 0x41, 0x9f, 0x50, 0x30, 0xc0, 0x47, 0x70, 0x65, 0xf8, 0x91, 0x3c, 0xe3, 0xe5, 0xd3, + 0xf2, 0x26, 0x76, 0x26, 0xab, 0x6c, 0x87, 0x01, 0x4e, 0xc5, 0x6a, 0x11, 0x27, 0x80, 0xa4, 0x14, + 0xc4, 0xd5, 0xfb, 0x80, 0x97, 0xc8, 0x46, 0xb7, 0xc7, 0x0f, 0xe1, 0xca, 0x95, 0x2b, 0x9d, 0x0c, + 0x3b, 0x56, 0x61, 0xe4, 0x39, 0x37, 0xef, 0xeb, 0x3e, 0xcc, 0x72, 0x0b, 0x52, 0x1d, 0xea, 0x39, + 0x8a, 0x59, 0x46, 0x78, 0xb0, 0x98, 0xe2, 0xfe, 0x7f, 0xe3, 0x40, 0x81, 0x66, 0x35, 0x1f, 0x8e, + 0x75, 0x2a, 0x2f, 0xb7, 0x0d, 0x37, 0x6a, 0x71, 0x8d, 0xb3, 0xef, 0xe1, 0x5c, 0x5d, 0x20, 0xf4, + 0xf6, 0x59, 0x1c, 0x75, 0x83, 0x1a, 0xfa, 0x4f, 0x80, 0x72, 0xb6, 0x50, 0x6f, 0xfc, 0xb3, 0x70, + 0xcb, 0x68, 0x8a, 0xb4, 0x06, 0x02, 0x3f, 0x33, 0xa4, 0x0e, 0x05, 0xd9, 0x25, 0xeb, 0x7e, 0x35, + 0x24, 0xd2, 0x47, 0x64, 0x07, 0x4e, 0xf5, 0x65, 0x4e, 0x16, 0xcf, 0xaa, 0xfe, 0x4a, 0xbe, 0xc3, + 0xb7, 0x7a, 0xd4, 0xaa, 0xf1, 0x24, 0x56, 0x60, 0xc8, 0x24, 0x07, 0x03, 0x01, 0xfd, 0x3d, 0x18, + 0xec, 0x09, 0x1c, 0xec, 0x63, 0x74, 0x5b, 0xe7, 0x1b, 0x5c, 0x52, 0x30, 0x09, 0x00, 0xa6, 0xbf, + 0x6b, 0x46, 0x26, 0xdf, 0x8c, 0x87, 0xde, 0x48, 0x42, 0x29, 0x48, 0x78, 0x55, 0xfb, 0x51, 0xda, + 0xe3, 0x82, 0xfc, 0xfd, 0x21, 0x58, 0xb9, 0x7b, 0x17, 0xd0, 0x0a, 0x6a, 0xeb, 0x5a, 0xce, 0xdc, + 0x71, 0x10, 0x03, 0xe4, 0x6b, 0x14, 0x4e, 0xda, 0x4e, 0xad, 0x9d, 0xa7, 0x63, 0x6f, 0x71, 0x23, + 0xf6, 0x43, 0x0b, 0x43, 0x31, 0x71, 0xfb, 0x7e, 0x8d, 0x49, 0x0c, 0x1e, 0x37, 0x3d, 0x52, 0xad, + 0xdb, 0xb7, 0x3a, 0x53, 0x13, 0xf7, 0x64, 0x4d, 0x3a, 0xf5, 0x6b, 0x45, 0x2d, 0xd3, 0xe0, 0x80, + 0x16, 0xd5, 0xf4, 0x88, 0x2e, 0xbd, 0xc2, 0x23, 0x35, 0xe9, 0x73, 0xfa, 0x4c, 0x49, 0x63, 0x69, + 0x8c, 0x60, 0x6d, 0x21, 0xdf, 0x9b, 0xff, 0xbf, 0xcc, 0xbc, 0x0f, 0xfa, 0x07, 0xa7, 0x6a, 0xcd, + 0x43, 0x5b, 0xd5, 0xa3, 0x75, 0x16, 0xa2, 0x9a, 0x10, 0x70, 0x79, 0x00, 0x00, 0x01, 0x01, 0x00, + 0xc8, 0xcd, 0xa4, 0x89, 0xf0, 0x84, 0x21, 0x20, 0x16, 0x54, 0x63, 0xa4, 0x1b, 0xcc, 0x68, 0xb9, + 0x4e, 0x46, 0x1a, 0xdc, 0xb1, 0x8a, 0x32, 0x24, 0xae, 0x1c, 0xa7, 0x1c, 0x77, 0xfb, 0xd8, 0x37, + 0xa4, 0x5b, 0x3c, 0x98, 0x96, 0xd5, 0x11, 0xe0, 0x45, 0xc7, 0xa1, 0xfb, 0xc3, 0x6d, 0x08, 0xf4, + 0x0d, 0xf8, 0x13, 0x63, 0x50, 0xf3, 0x93, 0x71, 0x25, 0x47, 0x99, 0xe5, 0x80, 0x3e, 0x62, 0x43, + 0x77, 0x3d, 0x58, 0x49, 0xc8, 0x4d, 0xae, 0xb0, 0x2f, 0x3c, 0x5e, 0x08, 0x97, 0x3a, 0xc7, 0x5f, + 0x89, 0x3c, 0x44, 0xf0, 0xaa, 0xe9, 0xeb, 0xf4, 0x9a, 0x2d, 0x5c, 0xd4, 0xa7, 0x26, 0xaa, 0xd5, + 0x18, 0xec, 0xd9, 0xc9, 0x0f, 0xde, 0xcd, 0xcc, 0xbd, 0xe4, 0xa3, 0x62, 0xed, 0xc0, 0x89, 0xa9, + 0x19, 0xb4, 0x4e, 0xc7, 0x89, 0xf9, 0x2f, 0x2a, 0x39, 0x71, 0xfb, 0x00, 0xf8, 0x54, 0x45, 0x73, + 0xfe, 0x77, 0x96, 0x32, 0x5a, 0xee, 0xf5, 0x53, 0xc2, 0x62, 0x13, 0x6d, 0x2d, 0x9d, 0x7e, 0xf6, + 0x09, 0xf2, 0xd6, 0xf5, 0xb5, 0x32, 0x67, 0x3c, 0x4d, 0xf7, 0x02, 0x45, 0xf7, 0x61, 0x9b, 0x5a, + 0x4e, 0x67, 0x2c, 0x7c, 0xeb, 0x2d, 0xde, 0x34, 0xa8, 0xc7, 0xfe, 0x1c, 0x4d, 0x0f, 0x99, 0x13, + 0xe2, 0xef, 0x3d, 0x0b, 0xf3, 0x05, 0x79, 0x9d, 0x79, 0x7c, 0x70, 0xda, 0xfe, 0xb8, 0xea, 0x5d, + 0xa0, 0x9d, 0x3c, 0xea, 0xc6, 0xe2, 0xc3, 0x9c, 0x42, 0x67, 0xba, 0x0b, 0x78, 0x68, 0xae, 0x5d, + 0x49, 0xd1, 0x61, 0x6f, 0xe9, 0x7f, 0x84, 0x51, 0x38, 0x7d, 0x29, 0xfb, 0x9a, 0x3e, 0x06, 0x9d, + 0xc1, 0x48, 0xe8, 0xb3, 0xff, 0xf3, 0x1e, 0x10, 0xec, 0x85, 0x99, 0xb5, 0x8b, 0xdd, 0xa6, 0xd6, + 0xce, 0xe3, 0x92, 0x3f, 0x74, 0x50, 0x45, 0xc1, 0x80, 0xc3, 0x3b, 0x3e, 0x87, 0xd8, 0x34, 0xae, + 0x00, 0x00, 0x01, 0x01, 0x00, 0xf2, 0x1c, 0x2d, 0x0f, 0xc1, 0x24, 0xd0, 0xd6, 0x88, 0xcb, 0x89, + 0xb4, 0x73, 0xb6, 0x31, 0xfc, 0x19, 0x0d, 0x5c, 0x46, 0x7f, 0x9c, 0xbd, 0xd6, 0x51, 0x64, 0xd5, + 0xaf, 0xbd, 0x0e, 0x40, 0xdb, 0x25, 0x5a, 0x8d, 0x65, 0xc6, 0xcd, 0x2a, 0x8f, 0x76, 0x8a, 0x24, + 0x66, 0xe5, 0x7f, 0xf8, 0x3a, 0xb3, 0xf4, 0x7f, 0xf2, 0x8d, 0x55, 0xdc, 0x12, 0x29, 0xb5, 0x08, + 0xa3, 0xae, 0xf2, 0xba, 0x69, 0xf8, 0x70, 0xb3, 0x5f, 0xab, 0x5b, 0x2f, 0x07, 0xf5, 0x88, 0xf4, + 0x10, 0x4e, 0xbf, 0x40, 0x88, 0xe3, 0xc3, 0x6a, 0x5d, 0x76, 0xc7, 0xf2, 0xb7, 0xdb, 0xf4, 0xfc, + 0x6c, 0xcf, 0x85, 0x88, 0xa8, 0x3b, 0x2b, 0x31, 0xfe, 0xc3, 0xc8, 0x33, 0x46, 0xaf, 0x5c, 0x74, + 0x15, 0xf7, 0xdf, 0x30, 0x84, 0xb4, 0x4b, 0x42, 0xad, 0x4a, 0xb2, 0xb6, 0x1d, 0x8c, 0x94, 0x18, + 0x10, 0x65, 0x27, 0x90, 0xea, 0x4e, 0x51, 0x6e, 0xe4, 0x7e, 0xaa, 0xb2, 0x04, 0x8a, 0x7b, 0xa0, + 0x62, 0xef, 0x96, 0x1a, 0x13, 0x6e, 0x04, 0x0a, 0x76, 0x8d, 0xc7, 0x36, 0xf6, 0xb1, 0xc4, 0x70, + 0x05, 0x3a, 0x7e, 0x55, 0xbe, 0xba, 0x6c, 0x7a, 0xa0, 0x53, 0x8f, 0xb2, 0x86, 0x96, 0xa5, 0x38, + 0x56, 0x16, 0xd1, 0x9b, 0xf7, 0x3e, 0x51, 0x23, 0x4e, 0x01, 0x31, 0x55, 0x0f, 0x4c, 0x5e, 0x45, + 0x3b, 0x41, 0x56, 0xfa, 0x3b, 0x4a, 0x09, 0x38, 0x28, 0xe9, 0x16, 0x68, 0xdb, 0x58, 0x49, 0xc3, + 0x57, 0x7f, 0x42, 0x47, 0x76, 0xb9, 0x8d, 0x92, 0xf9, 0x3f, 0xb0, 0xf3, 0x1c, 0xbe, 0x0d, 0xea, + 0xcf, 0xf9, 0x97, 0xf6, 0x94, 0xbd, 0x86, 0xed, 0xd2, 0x04, 0x02, 0xbb, 0x8a, 0xa9, 0xdf, 0x37, + 0x11, 0x0f, 0x3d, 0x95, 0xa2, 0xe2, 0xa2, 0x17, 0x1f, 0x6e, 0x4a, 0x2f, 0x1e, 0x94, 0xbf, 0xef, + 0x0c, 0x56, 0x5d, 0x42, 0x03, 0x00, 0x00, 0x01, 0x01, 0x00, 0xc2, 0x05, 0xc8, 0x82, 0x2b, 0xc2, + 0xa3, 0x14, 0x2f, 0xa2, 0x88, 0xe8, 0x01, 0x77, 0xc5, 0x03, 0x51, 0x65, 0xd6, 0xc2, 0x54, 0xf3, + 0x88, 0x72, 0x05, 0x65, 0x33, 0xae, 0x84, 0x25, 0xb9, 0xb7, 0x26, 0xae, 0x2e, 0x96, 0x84, 0xf7, + 0x6b, 0x73, 0xc3, 0x13, 0x76, 0x72, 0x05, 0x1c, 0x21, 0x06, 0x50, 0xc0, 0xd9, 0x52, 0xfc, 0xd3, + 0x0f, 0xd3, 0x0a, 0x68, 0xdc, 0xbd, 0xf9, 0xe4, 0xc9, 0xaa, 0x61, 0x1e, 0xc0, 0x56, 0x00, 0xc0, + 0x5d, 0xf7, 0xdf, 0xd3, 0x87, 0x8a, 0x7f, 0xa6, 0xec, 0xd8, 0x03, 0x21, 0x57, 0x74, 0x47, 0x88, + 0xb0, 0x4f, 0x4e, 0x98, 0x72, 0xf1, 0xf9, 0xd8, 0x65, 0xa2, 0x61, 0xfa, 0x83, 0x11, 0xe9, 0x77, + 0x43, 0xf1, 0xfb, 0x4f, 0x2d, 0x06, 0x9f, 0x8a, 0xec, 0x59, 0xb0, 0xcd, 0x33, 0x88, 0x9c, 0x1f, + 0x9c, 0xbc, 0xe3, 0xf4, 0x34, 0x04, 0xf8, 0xdc, 0x5c, 0x26, 0xd3, 0x6e, 0x91, 0xf3, 0x9a, 0x69, + 0xb9, 0x22, 0xde, 0x43, 0xf4, 0x6f, 0xcc, 0x41, 0x4e, 0x9d, 0x40, 0xad, 0xfe, 0xd5, 0x3d, 0xbb, + 0x6c, 0x22, 0x62, 0x0e, 0xa2, 0x09, 0xc1, 0xb8, 0xd9, 0x50, 0xe8, 0xe4, 0x1e, 0x74, 0x25, 0xf5, + 0x9e, 0x62, 0x55, 0xe5, 0x1b, 0xb4, 0x7e, 0x5c, 0x8b, 0x2d, 0x10, 0xa1, 0x8b, 0x12, 0x66, 0x3f, + 0xad, 0xb4, 0x84, 0xe4, 0xa4, 0x07, 0x7a, 0x9f, 0x8f, 0x7e, 0x04, 0xec, 0xf2, 0x38, 0x5c, 0x67, + 0x08, 0x0c, 0xae, 0x19, 0xea, 0xac, 0xf1, 0x80, 0xbb, 0x19, 0xe1, 0xb8, 0x1a, 0x7f, 0x49, 0x6f, + 0x2a, 0x9e, 0x8c, 0x68, 0xb8, 0x15, 0xd7, 0x6c, 0xaa, 0x4f, 0xed, 0x8f, 0x63, 0x39, 0x6b, 0x8d, + 0xb1, 0xa4, 0xbd, 0x84, 0x0a, 0xff, 0x34, 0x8f, 0x8c, 0xa5, 0x27, 0xf2, 0xca, 0x11, 0x28, 0x79, + 0x0d, 0x10, 0x6e, 0x58, 0x0d, 0xda, 0x05, 0x07, 0x54, 0x3d, +]; + +#[test] +fn legit_rsa_key_works() { + let bytes = bytes::Bytes::from(RSA_TEST_KEY); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + if let Err(ref e) = result { + println!("error = {:?}", e); + } + assert!(matches!(result, Ok(PrivateKey::Rsa(_, _)))); +} + +#[test] +fn successful_read_leaves_excess() { + let mut test_key = RSA_TEST_KEY.to_vec(); + test_key.push(0xa1); + test_key.push(0x23); + test_key.push(0x05); + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + if let Err(ref e) = result { + println!("error = {:?}", e); + } + assert!(matches!(result, Ok(PrivateKey::Rsa(_, _)))); + assert_eq!(buffer.get_u8().unwrap(), 0xa1); + assert_eq!(buffer.get_u8().unwrap(), 0x23); + assert_eq!(buffer.get_u8().unwrap(), 0x05); + assert!(!buffer.has_remaining()); +} + +#[test] +fn short_rsa_reads_fail_properly() { + let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..23]); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"n" + ))); + + let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..529]); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"e" + ))); + + let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..535]); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"d" + ))); + + let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..1247]); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"q⁻¹" + ))); + + let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..1550]); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"p" + ))); + + let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..1750]); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"q" + ))); +} + +#[test] +fn bad_rsa_key_is_bad() { + let mut test_key = RSA_TEST_KEY.to_vec(); + test_key[20] += 1; + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::BadRsaKey))); +} + +#[cfg(test)] +const ED25519_TEST_KEY: &[u8] = &[ + 0x00, 0x00, 0x00, 0x0b, 0x73, 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x00, + 0x00, 0x00, 0x20, 0x80, 0xe2, 0x47, 0x6a, 0x6f, 0xcb, 0x13, 0x7a, 0x0e, 0xda, 0x9b, 0x06, 0x3c, + 0x4d, 0xd7, 0x24, 0xdb, 0x31, 0x1b, 0xa9, 0xc5, 0xc3, 0x44, 0x5b, 0xda, 0xff, 0x85, 0x51, 0x15, + 0x63, 0x58, 0xd3, 0x00, 0x00, 0x00, 0x40, 0x7e, 0x5b, 0xf2, 0x9c, 0x9c, 0xea, 0xdf, 0x7f, 0x2a, + 0xf5, 0xf1, 0x3d, 0x46, 0xb6, 0xd5, 0xbc, 0x67, 0xac, 0xae, 0xb5, 0x17, 0xaa, 0x56, 0x22, 0x24, + 0x9b, 0xa7, 0x20, 0x39, 0x40, 0x00, 0xed, 0x80, 0xe2, 0x47, 0x6a, 0x6f, 0xcb, 0x13, 0x7a, 0x0e, + 0xda, 0x9b, 0x06, 0x3c, 0x4d, 0xd7, 0x24, 0xdb, 0x31, 0x1b, 0xa9, 0xc5, 0xc3, 0x44, 0x5b, 0xda, + 0xff, 0x85, 0x51, 0x15, 0x63, 0x58, 0xd3, +]; + +#[test] +fn good_ed25519_key_works() { + let bytes = bytes::Bytes::from(ED25519_TEST_KEY); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Ok(PrivateKey::Ed25519(_, _)))); + assert!(!buffer.has_remaining()); +} + +#[test] +fn short_ed25519_reads_fail_properly() { + let bytes = bytes::Bytes::from(&ED25519_TEST_KEY[0..20]); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PrivateKeyReadError::CouldNotReadEd25519 { part } if + part == &"public"))); + + let bytes = bytes::Bytes::from(&ED25519_TEST_KEY[0..60]); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PrivateKeyReadError::CouldNotReadEd25519 { part } if + part == &"private"))); +} + +#[test] +fn catch_invalid_ed25519_public() { + let mut test_key = ED25519_TEST_KEY.to_vec(); + test_key[19] = 0; + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PrivateKeyReadError::InvalidEd25519Key { kind, .. } if + kind == &"public"))); +} + +#[test] +fn catch_short_ed25519_length() { + let mut test_key = ED25519_TEST_KEY.to_vec(); + test_key[54] -= 1; + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PrivateKeyReadError::InvalidEd25519Key { kind, error } if + kind == &"private" && error.contains("key should be")))); +} + +#[test] +fn catch_invalid_private_key() { + let mut test_key = ED25519_TEST_KEY.to_vec(); + test_key[110] -= 1; + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PrivateKeyReadError::InvalidEd25519Key { kind, error } if + kind == &"private" && error.contains("final load")))); +} + +#[cfg(test)] +const P256_TEST_KEY: &[u8] = &[ + 0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e, + 0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70, + 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x41, 0x04, 0xd7, 0x47, 0x00, 0x93, 0x35, 0xc5, 0x88, 0xc1, + 0x67, 0xb5, 0x1d, 0x5f, 0xf1, 0x9b, 0x82, 0x1d, 0xe8, 0x37, 0x21, 0xe7, 0x89, 0xe5, 0x7c, 0x14, + 0x6a, 0xd7, 0xfe, 0x43, 0x44, 0xe7, 0x67, 0xd8, 0x05, 0x66, 0xe1, 0x96, 0x12, 0x8f, 0xc9, 0x23, + 0x1c, 0x8f, 0x25, 0x0e, 0xa7, 0xf1, 0xcd, 0x76, 0x7a, 0xea, 0xb7, 0x87, 0x24, 0x07, 0x1e, 0x72, + 0x63, 0x6b, 0x81, 0xde, 0x20, 0x81, 0xe7, 0x82, 0x00, 0x00, 0x00, 0x21, 0x00, 0xd1, 0x3d, 0x96, + 0x67, 0x38, 0xdd, 0xa7, 0xe9, 0x8d, 0x87, 0x6d, 0x6b, 0x98, 0x6f, 0x36, 0x8e, 0x87, 0x82, 0x6b, + 0x3a, 0x40, 0x2d, 0x99, 0x88, 0xf3, 0x26, 0x76, 0xf7, 0xe1, 0x3f, 0xff, 0x26, +]; + +#[test] +fn legit_p256_key_works() { + let bytes = bytes::Bytes::from(P256_TEST_KEY); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Ok(PrivateKey::P256(_, _)))); +} + +#[test] +fn check_for_mismatched_curves() { + let mut test_key = P256_TEST_KEY.to_vec(); + test_key[32] = '3' as u8; + test_key[33] = '8' as u8; + test_key[34] = '4' as u8; + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::MismatchedKeyAndCurve { key, curve } if + key == &"ecdsa-sha2-nistp256" && curve == &"nistp384"))); +} + +#[test] +fn ecc_short_reads_fail_correctly() { + let bytes = bytes::Bytes::from(&P256_TEST_KEY[0..48]); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::CouldNotReadPublicPoint { .. }))); + + let bytes = bytes::Bytes::from(&P256_TEST_KEY[0..112]); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::CouldNotReadPrivateScalar { .. }))); +} + +#[test] +fn p256_long_scalar_fails() { + let mut test_key = P256_TEST_KEY.to_vec(); + assert_eq!(0x21, test_key[107]); + test_key[107] += 2; + test_key.push(0); + test_key.push(0); + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::InvalidScalar { .. }))); +} + +#[test] +fn invalid_p256_fails_appropriately() { + let mut test_key = P256_TEST_KEY.to_vec(); + assert_eq!(4, test_key[39]); + test_key[39] = 0x33; + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + if let Err(ref e) = result { + println!("error: {:?}", e); + } + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::PointDecodeError { .. }))); + + let mut test_key = P256_TEST_KEY.to_vec(); + test_key[64] = 0x33; + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::BadPointForPublicKey { .. }))); + + let mut test_key = P256_TEST_KEY.to_vec(); + assert_eq!(0x21, test_key[107]); + test_key[107] = 0x22; + test_key.push(4); + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::InvalidScalar { .. }))); +} + +#[cfg(test)] +const P384_TEST_KEY: &[u8] = &[ + 0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e, + 0x69, 0x73, 0x74, 0x70, 0x33, 0x38, 0x34, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70, + 0x33, 0x38, 0x34, 0x00, 0x00, 0x00, 0x61, 0x04, 0x0d, 0xa3, 0x8b, 0x42, 0x98, 0x15, 0xba, 0x0c, + 0x9b, 0xf6, 0x5e, 0xc8, 0x68, 0xc3, 0x1e, 0x44, 0xb2, 0x6f, 0x12, 0x2f, 0xc8, 0x97, 0x81, 0x23, + 0x60, 0xa0, 0xc3, 0xaf, 0xf1, 0x3f, 0x5f, 0xd6, 0xea, 0x49, 0x9c, 0xd6, 0x74, 0x34, 0xd0, 0x6a, + 0xd0, 0x34, 0xe4, 0xd8, 0x42, 0x00, 0x94, 0x61, 0x63, 0x15, 0x11, 0xb0, 0x63, 0x52, 0xcc, 0xbe, + 0xe5, 0xc2, 0x12, 0x33, 0xdc, 0x36, 0x03, 0x60, 0x6c, 0xb9, 0x11, 0xa6, 0xe4, 0x81, 0x64, 0x4a, + 0x54, 0x74, 0x2b, 0xfb, 0xbc, 0xff, 0x90, 0xe0, 0x2c, 0x00, 0xc1, 0xae, 0x99, 0x2e, 0x0f, 0xdb, + 0x50, 0xec, 0x4c, 0xe8, 0xbd, 0xf1, 0x0f, 0xdc, 0x00, 0x00, 0x00, 0x30, 0x55, 0xc0, 0x13, 0xb0, + 0x61, 0x6d, 0xca, 0xf8, 0x09, 0x6f, 0x71, 0x26, 0x16, 0x97, 0x9b, 0x84, 0xe8, 0x37, 0xa9, 0x55, + 0xab, 0x73, 0x8a, 0xc3, 0x80, 0xb8, 0xd5, 0x9c, 0x71, 0x21, 0xeb, 0x4b, 0xc5, 0xf6, 0x21, 0xc9, + 0x92, 0x0c, 0xa6, 0x43, 0x48, 0x97, 0x18, 0x6c, 0x4f, 0x92, 0x42, 0xba, +]; + +#[test] +fn legit_p384_key_works() { + let bytes = bytes::Bytes::from(P384_TEST_KEY); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Ok(PrivateKey::P384(_, _)))); +} + +#[test] +fn p384_long_scalar_fails() { + let mut test_key = P384_TEST_KEY.to_vec(); + assert_eq!(0x30, test_key[139]); + test_key[139] += 2; + test_key.push(0); + test_key.push(0); + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::InvalidScalar { .. }))); +} + +#[test] +fn invalid_p384_fails_appropriately() { + let mut test_key = P384_TEST_KEY.to_vec(); + assert_eq!(4, test_key[39]); + test_key[39] = 0x33; + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::PointDecodeError { .. }))); + + let mut test_key = P384_TEST_KEY.to_vec(); + test_key[64] = 0x33; + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::BadPointForPublicKey { .. }))); + + let mut test_key = P384_TEST_KEY.to_vec(); + assert_eq!(0x30, test_key[139]); + test_key[139] = 0x31; + test_key.push(4); + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::InvalidScalar { .. }))); +} + +#[cfg(test)] +const P521_TEST_KEY: &[u8] = &[ + 0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e, + 0x69, 0x73, 0x74, 0x70, 0x35, 0x32, 0x31, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70, + 0x35, 0x32, 0x31, 0x00, 0x00, 0x00, 0x85, 0x04, 0x01, 0x68, 0x9a, 0x37, 0xac, 0xa3, 0x16, 0x26, + 0xa4, 0xaa, 0x72, 0xe6, 0x24, 0x40, 0x4c, 0x69, 0xbf, 0x11, 0x9e, 0xcd, 0xb6, 0x63, 0x92, 0x10, + 0xa6, 0xb7, 0x6e, 0x98, 0xb4, 0xa0, 0x81, 0xc5, 0x3c, 0x88, 0xfa, 0x9b, 0x60, 0x57, 0x4c, 0x0f, + 0xba, 0x36, 0x4e, 0xc6, 0xe0, 0x3e, 0xa5, 0x86, 0x3d, 0xd3, 0xd5, 0x86, 0x96, 0xe9, 0x4a, 0x1c, + 0x0c, 0xe2, 0x70, 0xff, 0x1f, 0x79, 0x06, 0x5d, 0x52, 0x9a, 0x01, 0x2b, 0x87, 0x8e, 0xc2, 0xe9, + 0xe2, 0xb7, 0x01, 0x00, 0xa6, 0x1a, 0xf7, 0x23, 0x47, 0x6a, 0x70, 0x10, 0x09, 0x59, 0xde, 0x0a, + 0x20, 0xca, 0x2f, 0xd7, 0x5a, 0x98, 0xbd, 0xc3, 0x5b, 0xf2, 0x7b, 0x14, 0x6e, 0x6b, 0xa5, 0x93, + 0x5d, 0x3e, 0x21, 0x5c, 0x49, 0x40, 0xbf, 0x9b, 0xc0, 0x78, 0x4b, 0xb1, 0xe9, 0xc7, 0x02, 0xb1, + 0x51, 0x94, 0x1a, 0xcf, 0x88, 0x7b, 0xfe, 0xea, 0xd8, 0x55, 0x89, 0xb3, 0x00, 0x00, 0x00, 0x42, + 0x01, 0x2d, 0xde, 0x75, 0x5b, 0x7a, 0x04, 0x7e, 0x24, 0xfc, 0x21, 0x07, 0xec, 0xf1, 0xab, 0xb0, + 0x21, 0xd6, 0x22, 0xa7, 0xb9, 0x77, 0x72, 0x34, 0x9c, 0xad, 0x32, 0x6f, 0x4f, 0xcc, 0xeb, 0x42, + 0xff, 0x4b, 0x9f, 0x78, 0x21, 0x6d, 0xbd, 0x61, 0xc1, 0xe0, 0x9d, 0xb4, 0xca, 0xc0, 0x22, 0xb1, + 0xd1, 0xdf, 0xad, 0xe7, 0xed, 0xc4, 0x90, 0x61, 0xe7, 0x7c, 0xab, 0x4a, 0xa9, 0x85, 0x60, 0xd9, + 0xad, 0x92, +]; + +#[test] +fn legit_p521_key_works() { + let bytes = bytes::Bytes::from(P521_TEST_KEY); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Ok(PrivateKey::P521(_, _)))); +} + +#[test] +fn p521_long_scalar_fails() { + let mut test_key = P521_TEST_KEY.to_vec(); + assert_eq!(0x42, test_key[175]); + test_key[175] += 2; + test_key.push(0); + test_key.push(0); + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::InvalidScalar { .. }))); +} + +#[test] +fn invalid_p521_fails_appropriately() { + let mut test_key = P521_TEST_KEY.to_vec(); + assert_eq!(4, test_key[39]); + test_key[39] = 0x33; + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::PointDecodeError { .. }))); + + let mut test_key = P521_TEST_KEY.to_vec(); + test_key[64] = 0x33; + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::BadPointForPublicKey { .. }))); + + let mut test_key = P521_TEST_KEY.to_vec(); + assert_eq!(0x42, test_key[175]); + test_key[175] = 0x43; + test_key.push(4); + let bytes = bytes::Bytes::from(test_key); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::InvalidScalar { .. }))); +} + +#[test] +fn dont_parse_unknown_key_types() { + let bytes = bytes::Bytes::from(b"\0\0\0\x07ssh-dsa\0\0\0\0".to_vec()); + let mut buffer = SshReadBuffer::from(bytes); + let result = read_private_key(&mut buffer); + assert!(matches!(result, Err(e) if + matches!( + e.current_context(), + PrivateKeyReadError::UnrecognizedKeyType { key_type } if + key_type.as_str() == "ssh-dsa"))); +} diff --git a/src/encodings/ssh/private_key_file.rs b/src/encodings/ssh/private_key_file.rs new file mode 100644 index 0000000..5e34312 --- /dev/null +++ b/src/encodings/ssh/private_key_file.rs @@ -0,0 +1,816 @@ +use super::{PublicKey, PublicKeyLoadError}; +use crate::encodings::ssh::buffer::SshReadBuffer; +use crate::encodings::ssh::private_key::{read_private_key, PrivateKey}; +use aes::cipher::{KeyIvInit, StreamCipher}; +use base64::engine::{self, Engine}; +use bytes::{Buf, Bytes}; +use error_stack::{report, ResultExt}; +use generic_array::GenericArray; +use std::io; +use std::path::Path; + +type Aes256Ctr = ctr::Ctr64BE; + +#[derive(Debug, thiserror::Error)] +pub enum PrivateKeyLoadError { + #[error("Could not read private key from file: {error}")] + CouldNotRead { error: io::Error }, + #[error("Private key file is lacking necessary newlines.")] + FileLackingNewlines, + #[error("Could not find OpenSSL private key header")] + NoOpenSSLHeader, + #[error("Could not find OpenSSL private key trailer")] + NoOpenSSLTrailer, + #[error("Base64 decoding error: {error}")] + Base64 { error: base64::DecodeError }, + #[error("Could not find OpenSSL magic header")] + NoMagicHeader, + #[error("Could not decode OpenSSL magic header: {error}")] + CouldNotDecodeMagicHeader { error: std::str::Utf8Error }, + #[error("Unexpected magic value; expected 'openssh-key-v1', got {value}")] + UnexpectedMagicHeaderValue { value: String }, + #[error("Could not determine cipher for private key")] + CouldNotDetermineCipher, + #[error("Could not determine KDF for private key")] + CouldNotDetermineKdf, + #[error("Could not determine KDF options for private key")] + CouldNotDetermineKdfOptions, + #[error("Could not determine encoded public key count")] + CouldNotDeterminePublicKeyCount, + #[error("Could not decode encoded public key")] + CouldNotLoadEncodedPublic, + #[error(transparent)] + PublicKeyError(#[from] PublicKeyLoadError), + #[error("Failed to properly decrypt contents ({checkint1} != {checkint2}")] + DecryptionCheckError { checkint1: u32, checkint2: u32 }, + #[error("File may have been truncated; should have at least {reported_length} bytes, saw {remaining_bytes}")] + TruncationError { + reported_length: usize, + remaining_bytes: usize, + }, + #[error("Very short file; could not find length of private key space")] + CouldNotFindPrivateBufferLength, + #[error("Padding does not match OpenSSH's requirements")] + PaddingError, + #[error("{amount} bytes of extraneous data found at end of private key buffer")] + ExtraneousData { amount: usize }, + #[error("Private key does not match associated public key")] + MismatchedPublic, + #[error("Unknown private key encryption scheme '{scheme}'")] + UnknownEncryptionScheme { scheme: String }, + #[error("Could not find salt bytes for key derivation")] + CouldNotFindSaltBytes, + #[error("Could not find number of key derivation rounds")] + CouldNotFindKdfRounds, + #[error("Extraneous info in key derivation block")] + ExtraneousKdfInfo, + #[error("Error running key derivation: {error}")] + KeyDerivationError { error: bcrypt_pbkdf::Error }, + #[error("Internal error: hit empty encryption method way too late in decryption path")] + EmptyEncryptionWayTooLate, + #[error("Failed to decrypt encrypted data: {error}")] + StreamCipherError { + error: aes::cipher::StreamCipherError, + }, + #[error("Could not get {which} post-decryption check bytes")] + CouldNotGetCheckBytes { which: &'static str }, + #[error("Failed to load private key")] + CouldNotLoadEncodedPrivate, + #[error("Failed to load info string for private key")] + CouldNotLoadPrivateInfo, +} + +#[derive(Debug, PartialEq)] +enum KeyEncryptionMode { + None, + Aes256Ctr, +} + +impl KeyEncryptionMode { + fn key_and_iv_size(&self) -> usize { + self.key_size() + self.iv_size() + } + + fn key_size(&self) -> usize { + match self { + KeyEncryptionMode::None => 0, + KeyEncryptionMode::Aes256Ctr => 32, + } + } + + fn iv_size(&self) -> usize { + match self { + KeyEncryptionMode::None => 0, + KeyEncryptionMode::Aes256Ctr => 16, + } + } +} + +#[derive(Debug)] +enum KeyDerivationMethod { + None, + Bcrypt { salt: Bytes, rounds: u32 }, +} + +#[derive(Debug)] +struct FileEncryptionData { + encryption_mode: KeyEncryptionMode, + key_derivation_method: KeyDerivationMethod, +} + +pub async fn load_openssh_file_keys>( + path: P, + provided_password: &Option, +) -> error_stack::Result, PrivateKeyLoadError> { + let path = path.as_ref(); + + let binary_data = load_openssh_binary_data(path) + .await + .attach_printable_lazy(|| format!("in {}", path.display()))?; + let mut data_buffer = SshReadBuffer::from(binary_data); + let encryption_info = load_encryption_info(&mut data_buffer) + .attach_printable_lazy(|| format!("in {}", path.display()))?; + + let key_count = data_buffer + .get_u32() + .change_context_lazy(|| PrivateKeyLoadError::CouldNotDeterminePublicKeyCount) + .attach_printable_lazy(|| format!("in {}", path.display()))?; + + let mut public_keys = vec![]; + for _ in 0..key_count { + let encoded_public = data_buffer + .get_bytes() + .change_context_lazy(|| PrivateKeyLoadError::CouldNotLoadEncodedPublic) + .attach_printable_lazy(|| format!("in {}", path.display()))?; + let found_public = PublicKey::try_from(encoded_public) + .change_context_lazy(|| PrivateKeyLoadError::CouldNotLoadEncodedPublic) + .attach_printable_lazy(|| format!("in {}", path.display()))?; + + public_keys.push(found_public); + } + + let private_key_data = decrypt_private_blob(data_buffer, encryption_info, provided_password)?; + + let mut private_key_buffer = SshReadBuffer::from(private_key_data); + let checkint1 = private_key_buffer + .get_u32() + .change_context_lazy(|| PrivateKeyLoadError::CouldNotGetCheckBytes { which: "first" }) + .attach_printable_lazy(|| format!("in {}", path.display()))?; + let checkint2 = private_key_buffer + .get_u32() + .change_context_lazy(|| PrivateKeyLoadError::CouldNotGetCheckBytes { which: "second" }) + .attach_printable_lazy(|| format!("in {}", path.display()))?; + + if checkint1 != checkint2 { + return Err(report!(PrivateKeyLoadError::DecryptionCheckError { + checkint1, + checkint2, + })); + } + + let mut results = vec![]; + for public_key in public_keys.into_iter() { + let private = read_private_key(&mut private_key_buffer) + .change_context_lazy(|| PrivateKeyLoadError::CouldNotLoadEncodedPrivate) + .attach_printable_lazy(|| format!("in {}", path.display()))?; + + if private.public() != public_key { + return Err(report!(PrivateKeyLoadError::MismatchedPublic)); + } + + let private_info = private_key_buffer + .get_string() + .change_context_lazy(|| PrivateKeyLoadError::CouldNotLoadPrivateInfo) + .attach_printable_lazy(|| format!("in {}", path.display()))?; + + results.push((private, private_info)); + } + + let mut should_be = 1; + while let Ok(next) = private_key_buffer.get_u8() { + if next != should_be { + return Err(report!(PrivateKeyLoadError::PaddingError)) + .attach_printable_lazy(|| format!("in {}", path.display())); + } + should_be += 1; + } + + Ok(results) +} + +async fn load_openssh_binary_data(path: &Path) -> error_stack::Result { + let file_data = tokio::fs::read_to_string(path) + .await + .map_err(|error| report!(PrivateKeyLoadError::CouldNotRead { error }))?; + + let (openssh_header, everything_else) = file_data + .split_once('\n') + .ok_or_else(|| report!(PrivateKeyLoadError::FileLackingNewlines))?; + + let (actual_key_data, openssh_trailer) = everything_else + .trim_end() + .rsplit_once('\n') + .ok_or_else(|| report!(PrivateKeyLoadError::FileLackingNewlines))?; + + if openssh_header != "-----BEGIN OPENSSH PRIVATE KEY-----" { + return Err(report!(PrivateKeyLoadError::NoOpenSSLHeader)); + } + + if openssh_trailer != "-----END OPENSSH PRIVATE KEY-----" { + return Err(report!(PrivateKeyLoadError::NoOpenSSLTrailer)); + } + + let single_line_data: String = actual_key_data + .chars() + .filter(|x| !x.is_whitespace()) + .collect(); + let mut key_material = engine::general_purpose::STANDARD + .decode(single_line_data) + .map(Bytes::from) + .map_err(|de| report!(PrivateKeyLoadError::Base64 { error: de }))?; + + if key_material.remaining() < 15 { + return Err(report!(PrivateKeyLoadError::NoMagicHeader)); + } + + let auth_magic = std::str::from_utf8(&key_material[0..15]) + .map_err(|e| report!(PrivateKeyLoadError::CouldNotDecodeMagicHeader { error: e }))?; + + if auth_magic != "openssh-key-v1\0" { + return Err(report!(PrivateKeyLoadError::UnexpectedMagicHeaderValue { + value: auth_magic.to_string(), + })); + } + key_material.advance(15); + + Ok(key_material) +} + +fn load_encryption_info( + buffer: &mut SshReadBuffer, +) -> error_stack::Result { + let cipher_name = buffer + .get_string() + .change_context_lazy(|| PrivateKeyLoadError::CouldNotDetermineCipher)?; + + let encryption_mode = match cipher_name.as_str() { + "none" => KeyEncryptionMode::None, + "aes256-ctr" => KeyEncryptionMode::Aes256Ctr, + _ => { + return Err(report!(PrivateKeyLoadError::UnknownEncryptionScheme { + scheme: cipher_name, + })) + } + }; + + let kdf_name = buffer + .get_string() + .change_context_lazy(|| PrivateKeyLoadError::CouldNotDetermineKdf)?; + + let key_derivation_method = match kdf_name.as_str() { + "none" => { + let _ = buffer + .get_bytes() + .change_context_lazy(|| PrivateKeyLoadError::CouldNotDetermineKdfOptions)?; + KeyDerivationMethod::None + } + + "bcrypt" => { + let mut blob = buffer + .get_bytes() + .map(SshReadBuffer::from) + .change_context_lazy(|| PrivateKeyLoadError::CouldNotDetermineKdfOptions)?; + let salt = blob + .get_bytes() + .change_context_lazy(|| PrivateKeyLoadError::CouldNotFindSaltBytes)?; + let rounds = blob + .get_u32() + .change_context_lazy(|| PrivateKeyLoadError::CouldNotFindKdfRounds)?; + + if blob.has_remaining() { + return Err(report!(PrivateKeyLoadError::ExtraneousKdfInfo)); + } + + KeyDerivationMethod::Bcrypt { salt, rounds } + } + + _ => { + return Err(report!(PrivateKeyLoadError::UnknownEncryptionScheme { + scheme: kdf_name, + })) + } + }; + + Ok(FileEncryptionData { + encryption_mode, + key_derivation_method, + }) +} + +fn decrypt_private_blob( + mut buffer: SshReadBuffer, + encryption_info: FileEncryptionData, + provided_password: &Option, +) -> error_stack::Result { + let ciphertext = buffer + .get_bytes() + .change_context_lazy(|| PrivateKeyLoadError::CouldNotFindPrivateBufferLength)?; + + if buffer.has_remaining() { + return Err(report!(PrivateKeyLoadError::ExtraneousData { + amount: buffer.remaining(), + })); + } + + if encryption_info.encryption_mode == KeyEncryptionMode::None { + return Ok(ciphertext); + } + + let password = match provided_password { + Some(x) => x.as_str(), + None => unimplemented!(), + }; + + let mut key_and_iv = vec![0; encryption_info.encryption_mode.key_and_iv_size()]; + match encryption_info.key_derivation_method { + KeyDerivationMethod::None => { + for (idx, value) in password.as_bytes().iter().enumerate() { + if idx < key_and_iv.len() { + key_and_iv[idx] = *value; + } + } + } + + KeyDerivationMethod::Bcrypt { salt, rounds } => { + bcrypt_pbkdf::bcrypt_pbkdf(password, &salt, rounds, &mut key_and_iv) + .map_err(|error| report!(PrivateKeyLoadError::KeyDerivationError { error }))?; + } + } + + let key_size = encryption_info.encryption_mode.key_size(); + let key = GenericArray::from_slice(&key_and_iv[0..key_size]); + let iv = GenericArray::from_slice(&key_and_iv[key_size..]); + + match encryption_info.encryption_mode { + KeyEncryptionMode::None => Err(report!(PrivateKeyLoadError::EmptyEncryptionWayTooLate)), + + KeyEncryptionMode::Aes256Ctr => { + let mut out_buffer = vec![0u8; ciphertext.len()]; + let mut cipher = Aes256Ctr::new(key, iv); + + cipher + .apply_keystream_b2b(&ciphertext, &mut out_buffer) + .map_err(|error| PrivateKeyLoadError::StreamCipherError { error })?; + Ok(out_buffer.into()) + } + } +} + +#[test] +fn password_generation_matches_saved() { + let salt = [ + 0x1f, 0x77, 0xd3, 0x33, 0xcc, 0x9e, 0xd1, 0x45, 0xe3, 0xc1, 0xc5, 0x26, 0xa8, 0x7d, 0xf4, + 0x1a, + ]; + let key_and_iv = [ + 0xcd, 0xd5, 0x5f, 0x6c, 0x73, 0xa0, 0x5c, 0x46, 0x9d, 0xdd, 0x84, 0xbf, 0xab, 0x3a, 0xa6, + 0x6e, 0xd6, 0x18, 0xeb, 0x4e, 0x34, 0x1d, 0x89, 0x38, 0x92, 0x4a, 0x0b, 0x5c, 0xca, 0xba, + 0x3e, 0xed, 0x42, 0x2e, 0xd3, 0x1f, 0x0b, 0xb7, 0x22, 0x41, 0xeb, 0x3d, 0x37, 0x91, 0xf7, + 0x12, 0x15, 0x1b, + ]; + let password = "foo"; + let rounds = 24; + + let mut output = [0; 48]; + + bcrypt_pbkdf::bcrypt_pbkdf(password, &salt, rounds, &mut output).unwrap(); + assert_eq!(key_and_iv, output); +} + +#[test] +fn can_regenerate_and_decode_saved_session() { + let salt = [ + 0x86, 0x4a, 0xa5, 0x81, 0x73, 0xb1, 0x12, 0x37, 0x54, 0x6e, 0x34, 0x22, 0x84, 0x89, 0xba, + 0x8c, + ]; + let key_and_iv = [ + 0x6f, 0xda, 0x3b, 0x95, 0x1d, 0x85, 0xd7, 0xb0, 0x56, 0x8c, 0xc2, 0x4c, 0xa9, 0xf5, 0x95, + 0x4c, 0x9b, 0x39, 0x75, 0x14, 0x29, 0x32, 0xac, 0x2b, 0xd3, 0xf8, 0x63, 0x50, 0xc8, 0xfa, + 0xcb, 0xb4, 0xca, 0x9a, 0x53, 0xd1, 0xf1, 0x26, 0x26, 0xd8, 0x1a, 0x44, 0x76, 0x2b, 0x27, + 0xd0, 0x43, 0x91, + ]; + let key_length = 32; + let iv_length = 16; + let rounds = 24; + + let mut bcrypt_output = [0; 48]; + bcrypt_pbkdf::bcrypt_pbkdf("foo", &salt, rounds, &mut bcrypt_output).unwrap(); + assert_eq!(key_and_iv, bcrypt_output); + + let key_bytes = &key_and_iv[0..key_length]; + let iv_bytes = &key_and_iv[key_length..]; + assert_eq!(iv_length, iv_bytes.len()); + + let plaintext = [ + 0xfc, 0xc7, 0x14, 0xe5, 0xfc, 0xc7, 0x14, 0xe5, 0x00, 0x00, 0x00, 0x0b, 0x73, 0x73, 0x68, + 0x2d, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x00, 0x00, 0x00, 0x20, 0x85, 0x50, 0x4f, + 0x2e, 0xd7, 0xab, 0x62, 0xf0, 0xc5, 0xe1, 0xaf, 0x7f, 0x20, 0x6b, 0xb7, 0x3d, 0x92, 0x7d, + 0xa4, 0x00, 0xf9, 0xdd, 0x08, 0x38, 0x7b, 0xbf, 0x91, 0x3a, 0xd0, 0xfc, 0x00, 0x6d, 0x00, + 0x00, 0x00, 0x40, 0x23, 0xfe, 0xe2, 0xb9, 0xae, 0x83, 0x97, 0xa1, 0x7d, 0x4f, 0x45, 0xb2, + 0x61, 0x28, 0xeb, 0x6d, 0xd6, 0x5c, 0x38, 0x04, 0x2c, 0xbc, 0x9d, 0xf5, 0x1b, 0x47, 0x3b, + 0x89, 0x20, 0x77, 0x6c, 0x8c, 0x85, 0x50, 0x4f, 0x2e, 0xd7, 0xab, 0x62, 0xf0, 0xc5, 0xe1, + 0xaf, 0x7f, 0x20, 0x6b, 0xb7, 0x3d, 0x92, 0x7d, 0xa4, 0x00, 0xf9, 0xdd, 0x08, 0x38, 0x7b, + 0xbf, 0x91, 0x3a, 0xd0, 0xfc, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x10, 0x61, 0x64, 0x61, 0x6d, + 0x77, 0x69, 0x63, 0x6b, 0x40, 0x65, 0x72, 0x67, 0x61, 0x74, 0x65, 0x73, 0x01, 0x02, 0x03, + 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + ]; + + let ciphertext = [ + 0x19, 0x96, 0xad, 0x71, 0x8d, 0x07, 0x9d, 0xf3, 0x8d, 0xe9, 0x63, 0x1c, 0xfe, 0xe4, 0xc2, + 0x6a, 0x04, 0x12, 0xc1, 0x81, 0xc2, 0xe0, 0xd9, 0x63, 0xf1, 0xb8, 0xf1, 0x00, 0x6a, 0xb4, + 0x35, 0xc3, 0x7e, 0x71, 0xa5, 0x65, 0xab, 0x82, 0x66, 0xd9, 0x3e, 0x68, 0x69, 0xa3, 0x01, + 0xe1, 0x67, 0x42, 0x0a, 0x7c, 0xe2, 0x92, 0xab, 0x4f, 0x00, 0xfa, 0xaa, 0x20, 0x88, 0x6b, + 0xa7, 0x39, 0x75, 0x0f, 0xab, 0xf5, 0x53, 0x47, 0x07, 0x10, 0xb5, 0xfb, 0xf1, 0x86, 0x9e, + 0xbb, 0xe9, 0x22, 0x59, 0xa8, 0xdf, 0xf0, 0xa5, 0x28, 0xa5, 0x27, 0x26, 0x1b, 0x05, 0xb1, + 0xae, 0xb4, 0xbf, 0x15, 0xa5, 0xbf, 0x64, 0x8a, 0xb3, 0x9c, 0x11, 0x16, 0xa2, 0x01, 0xa7, + 0xfd, 0x2d, 0xfa, 0xc6, 0x01, 0xb2, 0xfd, 0xaa, 0x14, 0x38, 0x12, 0x79, 0xb1, 0x8a, 0x86, + 0xa8, 0xdb, 0x84, 0xe9, 0xc8, 0xbb, 0x37, 0x36, 0xe4, 0x7d, 0x89, 0xd2, 0x1b, 0xab, 0x79, + 0x68, 0x69, 0xb8, 0xe5, 0x04, 0x7a, 0x00, 0x14, 0x5a, 0xa5, 0x96, 0x0a, 0x1d, 0xe9, 0x5a, + 0xfc, 0x80, 0x77, 0x06, 0x4d, 0xb9, 0x02, 0x95, 0x2c, 0x34, + ]; + + let key = GenericArray::from_slice(&key_bytes); + let iv = GenericArray::from_slice(&iv_bytes); + + let mut conversion_buffer = [0; 160]; + let mut cipher = Aes256Ctr::new(key, iv); + cipher + .apply_keystream_b2b(&plaintext, &mut conversion_buffer) + .unwrap(); + assert_eq!(ciphertext, conversion_buffer); + + let mut cipher = Aes256Ctr::new(key, iv); + cipher + .apply_keystream_b2b(&ciphertext, &mut conversion_buffer) + .unwrap(); + assert_eq!(plaintext, conversion_buffer); +} + +#[tokio::test] +async fn test_keys_parse() { + let mut parsed_keys = vec![]; + let test_key_directory = format!("{}/tests/ssh_keys", env!("CARGO_MANIFEST_DIR")); + let mut directory_reader = tokio::fs::read_dir(test_key_directory) + .await + .expect("can read test key directory"); + + while let Ok(Some(entry)) = directory_reader.next_entry().await { + if entry.path().extension().is_none() + && entry + .file_type() + .await + .map(|x| x.is_file()) + .unwrap_or_default() + { + let mut private_keys = load_openssh_file_keys(entry.path(), &Some("hush".to_string())) + .await + .expect("can parse saved private key"); + + parsed_keys.append(&mut private_keys); + } + } + + assert_eq!(18, parsed_keys.len()); +} + +#[tokio::test] +async fn improper_newline_errors_work() { + use std::io::Write; + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + named_temp + .write_all("-----BEGIN OPENSSH PRIVATE KEY-----".as_bytes()) + .unwrap(); + let path = named_temp.into_temp_path(); + let result = load_openssh_binary_data(&path).await; + assert!(matches!( + result.unwrap_err().current_context(), + PrivateKeyLoadError::FileLackingNewlines + )); + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + named_temp + .write_all( + "-----BEGIN OPENSSH PRIVATE KEY-----\n-----END OPENSSH PRIVATE KEY-----".as_bytes(), + ) + .unwrap(); + let path = named_temp.into_temp_path(); + let result = load_openssh_binary_data(&path).await; + assert!(matches!( + result.unwrap_err().current_context(), + PrivateKeyLoadError::FileLackingNewlines + )); +} + +#[tokio::test] +async fn improper_header_trailer_errors_work() { + use std::io::Write; + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + named_temp + .write_all("-----BEGIN OPENSSH PRIVTE KEY-----\n".as_bytes()) + .unwrap(); + named_temp.write_all("stuff\n".as_bytes()).unwrap(); + named_temp + .write_all("-----END OPENSSH PRIVATE KEY-----\n".as_bytes()) + .unwrap(); + let path = named_temp.into_temp_path(); + let result = load_openssh_binary_data(&path).await; + assert!(matches!( + result.unwrap_err().current_context(), + PrivateKeyLoadError::NoOpenSSLHeader + )); + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + named_temp + .write_all("-----BEGIN OPENSSH PRIVATE KEY-----\n".as_bytes()) + .unwrap(); + named_temp.write_all("stuff\n".as_bytes()).unwrap(); + named_temp + .write_all("-----END OPENSSH PRIVATEKEY-----\n".as_bytes()) + .unwrap(); + let path = named_temp.into_temp_path(); + let result = load_openssh_binary_data(&path).await; + assert!(matches!( + result.unwrap_err().current_context(), + PrivateKeyLoadError::NoOpenSSLTrailer + )); +} + +#[tokio::test] +async fn invalid_initial_data_errors_work() { + use std::io::Write; + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap(); + writeln!(named_temp, "stuff").unwrap(); + writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap(); + let path = named_temp.into_temp_path(); + let result = load_openssh_binary_data(&path).await; + assert!(matches!( + result.unwrap_err().current_context(), + PrivateKeyLoadError::Base64 { .. } + )); + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap(); + writeln!( + named_temp, + "{}", + base64::engine::general_purpose::STANDARD.encode(b"openssl\x00") + ) + .unwrap(); + writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap(); + let path = named_temp.into_temp_path(); + let result = load_openssh_binary_data(&path).await; + assert!(matches!( + result.unwrap_err().current_context(), + PrivateKeyLoadError::NoMagicHeader + )); + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap(); + writeln!( + named_temp, + "{}", + base64::engine::general_purpose::STANDARD.encode(b"openssl\xc3\x28key-v1\x00") + ) + .unwrap(); + writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap(); + let path = named_temp.into_temp_path(); + let result = load_openssh_binary_data(&path).await; + assert!(matches!( + result.unwrap_err().current_context(), + PrivateKeyLoadError::CouldNotDecodeMagicHeader { .. } + )); + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap(); + writeln!(named_temp, "b3BlbnNzbC1rZXktdjFh").unwrap(); + writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap(); + let path = named_temp.into_temp_path(); + let result = load_openssh_binary_data(&path).await; + assert!(matches!(result.unwrap_err().current_context(), + PrivateKeyLoadError::UnexpectedMagicHeaderValue { value } + if value == "openssl-key-v1a")); +} + +#[test] +fn errors_on_weird_encryption_info() { + let mut example_bytes = SshReadBuffer::from(Bytes::from(b"\0\0\0\x09aes256ctr".to_vec())); + let result = load_encryption_info(&mut example_bytes).unwrap_err(); + assert!( + matches!(result.current_context(), PrivateKeyLoadError::UnknownEncryptionScheme { scheme } + if scheme == "aes256ctr") + ); + + let mut example_bytes = SshReadBuffer::from(Bytes::from(b"\0\0\0\x0aaes256-ctr".to_vec())); + let result = load_encryption_info(&mut example_bytes).unwrap_err(); + assert!(matches!( + result.current_context(), + PrivateKeyLoadError::CouldNotDetermineKdf + )); + + let mut example_bytes = + SshReadBuffer::from(Bytes::from(b"\0\0\0\x0aaes256-ctr\0\0\0\x03foo".to_vec())); + let result = load_encryption_info(&mut example_bytes).unwrap_err(); + assert!( + matches!(result.current_context(), PrivateKeyLoadError::UnknownEncryptionScheme { scheme } + if scheme == "foo") + ); + + let mut example_bytes = SshReadBuffer::from(Bytes::from( + b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\0".to_vec(), + )); + let result = load_encryption_info(&mut example_bytes).unwrap_err(); + assert!(matches!( + result.current_context(), + PrivateKeyLoadError::CouldNotFindSaltBytes + )); + + let mut example_bytes = SshReadBuffer::from(Bytes::from( + b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\x06\0\0\0\x04ab".to_vec(), + )); + let result = load_encryption_info(&mut example_bytes).unwrap_err(); + assert!(matches!( + result.current_context(), + PrivateKeyLoadError::CouldNotFindSaltBytes + )); + + let mut example_bytes = SshReadBuffer::from(Bytes::from( + b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\x06\0\0\0\x02ab".to_vec(), + )); + let result = load_encryption_info(&mut example_bytes).unwrap_err(); + assert!(matches!( + result.current_context(), + PrivateKeyLoadError::CouldNotFindKdfRounds + )); + + let mut example_bytes = SshReadBuffer::from(Bytes::from( + b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\x0b\0\0\0\x02ab\0\0\0\x02c".to_vec(), + )); + let result = load_encryption_info(&mut example_bytes).unwrap_err(); + assert!(matches!( + result.current_context(), + PrivateKeyLoadError::ExtraneousKdfInfo + )); +} + +#[test] +fn basic_kdf_examples_work() { + let mut no_encryption = SshReadBuffer::from(Bytes::from( + b"\0\0\0\x04none\0\0\0\x04none\0\0\0\0".to_vec(), + )); + let result = load_encryption_info(&mut no_encryption).unwrap(); + assert_eq!(KeyEncryptionMode::None, result.encryption_mode); + assert!(matches!( + result.key_derivation_method, + KeyDerivationMethod::None + )); + + let mut encryption = SshReadBuffer::from(Bytes::from( + b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\x0a\0\0\0\x02ab\0\0\0\x18".to_vec(), + )); + let result = load_encryption_info(&mut encryption).unwrap(); + assert_eq!(KeyEncryptionMode::Aes256Ctr, result.encryption_mode); + assert!( + matches!(result.key_derivation_method, KeyDerivationMethod::Bcrypt { salt, rounds } + if salt.as_ref() == b"ab" && rounds == 24) + ); +} + +#[cfg(test)] +pub fn generate_test_file(bytes: &[u8]) -> tempfile::TempPath { + use std::io::Write; + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap(); + writeln!( + named_temp, + "{}", + base64::engine::general_purpose::STANDARD.encode(bytes) + ) + .unwrap(); + writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap(); + named_temp.into_temp_path() +} + +#[tokio::test] +async fn invalid_encryption_info_stops_parsing() { + let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x09aes256ctr"); + let result = load_openssh_file_keys(&path, &None).await; + assert!( + matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::UnknownEncryptionScheme { .. })) + ); +} + +#[tokio::test] +async fn invalid_public_keys_stops_parsing() { + let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0"); + let result = load_openssh_file_keys(&path, &None).await; + assert!( + matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::CouldNotDeterminePublicKeyCount)) + ); + + let path = generate_test_file( + b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\x01\0\0\0\x03foo", + ); + let result = load_openssh_file_keys(&path, &None).await; + assert!( + matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::CouldNotLoadEncodedPublic)) + ); +} + +#[tokio::test] +async fn checkint_validation_works() { + let path = generate_test_file( + b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x02ab", + ); + let result = load_openssh_file_keys(&path, &None).await; + assert!( + matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::CouldNotGetCheckBytes { which } if which == &"first")) + ); + + let path = generate_test_file( + b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x06abcdef", + ); + let result = load_openssh_file_keys(&path, &None).await; + assert!( + matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::CouldNotGetCheckBytes { which } if which == &"second")) + ); + + let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x08\0\0\0\x01\0\0\0\x02"); + let result = load_openssh_file_keys(&path, &None).await; + assert!( + matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::DecryptionCheckError { checkint1, checkint2 } if *checkint1 == 1 && *checkint2 == 2)) + ); + + let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x08\0\0\0\x01\0\0\0\x01"); + let result = load_openssh_file_keys(&path, &None).await; + assert!(result.is_ok()); +} + +#[tokio::test] +async fn padding_checks_work() { + let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x08\0\0\0\x01\0\0\0\x01"); + let result = load_openssh_file_keys(&path, &None).await; + assert!(result.is_ok()); + + let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x09\0\0\0\x01\0\0\0\x01\x01"); + let result = load_openssh_file_keys(&path, &None).await; + assert!(result.is_ok()); + + let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x0a\0\0\0\x01\0\0\0\x01\x01\x02"); + let result = load_openssh_file_keys(&path, &None).await; + assert!(result.is_ok()); + + let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x09\0\0\0\x01\0\0\0\x01\x00"); + let result = load_openssh_file_keys(&path, &None).await; + assert!(result.is_err()); + + let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x0a\0\0\0\x01\0\0\0\x01\x01\x03"); + let result = load_openssh_file_keys(&path, &None).await; + assert!(result.is_err()); + + let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x0a\0\0\0\x01\0\0\0\x01\x01\x03\x04"); + let result = load_openssh_file_keys(&path, &None).await; + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PrivateKeyLoadError::ExtraneousData { amount } if + amount == &1))); +} + +#[tokio::test] +async fn file_errors_are_caught() { + let result = load_openssh_file_keys("--capitan--", &None).await; + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PrivateKeyLoadError::CouldNotRead { .. }))); +} + +#[tokio::test] +async fn mismatched_keys_are_handled() { + let test_path = format!( + "{}/tests/broken_keys/mismatched", + env!("CARGO_MANIFEST_DIR") + ); + let result = load_openssh_file_keys(test_path.as_str(), &None).await; + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PrivateKeyLoadError::MismatchedPublic))); +} + +#[tokio::test] +async fn broken_private_info_strings_are_handled() { + let test_path = format!("{}/tests/broken_keys/bad_info", env!("CARGO_MANIFEST_DIR")); + let result = load_openssh_file_keys(test_path.as_str(), &None).await; + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PrivateKeyLoadError::CouldNotLoadPrivateInfo))); +} diff --git a/src/encodings/ssh/public_key.rs b/src/encodings/ssh/public_key.rs new file mode 100644 index 0000000..d69f4bf --- /dev/null +++ b/src/encodings/ssh/public_key.rs @@ -0,0 +1,448 @@ +use crate::crypto::rsa; +use crate::encodings::ssh::buffer::SshReadBuffer; +use bytes::Bytes; +use elliptic_curve::sec1::FromEncodedPoint; +use error_stack::{report, ResultExt}; +use num_bigint_dig::BigUint; +use thiserror::Error; + +/// An SSH public key type. +/// +/// Technically, SSH supports additional key types not listed in this +/// enumeration, but we have chosen to be a little opinionated about +/// what constitutes a good key type. Note that SSH keys can also be +/// appended with additonal information in their various file types; +/// this code only processes the key material. +#[derive(Debug, PartialEq)] +pub enum PublicKey { + Rsa(rsa::PublicKey), + Ed25519(ed25519_dalek::VerifyingKey), + P256(p256::PublicKey), + P384(p384::PublicKey), + P521(p521::PublicKey), +} + +impl PublicKey { + /// Returns the string that SSH would use to describe this key type. + /// + /// It's not clear how standard these names are, but they are + /// associated with the output of OpenSSH, and appear to match + /// some of the strings listed in the SSH RFCs. + pub fn ssh_key_type_name(&self) -> &'static str { + match self { + PublicKey::Rsa(_) => "ssh-rsa", + PublicKey::P256(_) => "ecdsa-sha2-nistp256", + PublicKey::P384(_) => "ecdsa-sha2-nistp384", + PublicKey::P521(_) => "ecdsa-sha2-nistp521", + PublicKey::Ed25519(_) => "ssh-ed25519", + } + } +} + +/// Errors that can occur trying to read an SSH public key from a +/// binary blob. +#[derive(Debug, Error)] +pub enum PublicKeyReadError { + #[error("Could not read encoded public key type")] + NoPublicKeyType, + #[error("Unrecognized encoded public key type: {key_type}")] + UnrecognizedKeyType { key_type: String }, + #[error("Could not determine RSA public '{constant_name}' constant")] + CouldNotFindRsaConstant { constant_name: &'static str }, + #[error("Extraneous information at the end of public '{key_type}' key")] + ExtraneousInfo { key_type: &'static str }, + #[error("Could not find ed25519 public point")] + NoEd25519Data, + #[error("Invalid ed25519 public key value: {error}")] + InvalidEd25519Data { + error: ed25519_dalek::SignatureError, + }, + #[error("Could not read ECDSA curve information")] + CouldNotReadCurve, + #[error( + "Mismatched ECDSA curve info in public key, saw key type {key_type} but curve {curve}" + )] + MismatchedCurveInfo { key_type: String, curve: String }, + #[error("Could not read public {curve} point data")] + CouldNotReadEcdsaPoint { curve: String }, + #[error("Invalid ECDSA point for curve {curve}: {error}")] + InvalidEcdsaPoint { + curve: String, + error: elliptic_curve::Error, + }, + #[error("Invalid ECDSA public value for curve {curve}")] + InvalidEcdsaPublicValue { curve: String }, +} + +impl TryFrom for PublicKey { + type Error = error_stack::Report; + + fn try_from(value: Bytes) -> Result { + let mut ssh_buffer = SshReadBuffer::from(value); + + let encoded_key_type = ssh_buffer + .get_string() + .change_context(PublicKeyReadError::NoPublicKeyType)?; + match encoded_key_type.as_str() { + "ssh-rsa" => { + let ebytes = ssh_buffer.get_bytes().change_context_lazy(|| { + PublicKeyReadError::CouldNotFindRsaConstant { constant_name: "e" } + })?; + let nbytes = ssh_buffer.get_bytes().change_context_lazy(|| { + PublicKeyReadError::CouldNotFindRsaConstant { constant_name: "n" } + })?; + + if ssh_buffer.has_remaining() { + return Err(report!(PublicKeyReadError::ExtraneousInfo { + key_type: "RSA" + })); + } + + let e = BigUint::from_bytes_be(&ebytes); + let n = BigUint::from_bytes_be(&nbytes); + + Ok(PublicKey::Rsa(rsa::PublicKey::new(n, e))) + } + + "ssh-ed25519" => { + let point_bytes = ssh_buffer + .get_bytes() + .change_context(PublicKeyReadError::NoEd25519Data)?; + + if ssh_buffer.has_remaining() { + return Err(report!(PublicKeyReadError::ExtraneousInfo { + key_type: "ed25519" + })); + } + + let point = ed25519_dalek::VerifyingKey::try_from(point_bytes.as_ref()) + .map_err(|error| report!(PublicKeyReadError::InvalidEd25519Data { error }))?; + + Ok(PublicKey::Ed25519(point)) + } + + "ecdsa-sha2-nistp256" | "ecdsa-sha2-nistp384" | "ecdsa-sha2-nistp521" => { + let curve = ssh_buffer + .get_string() + .change_context(PublicKeyReadError::CouldNotReadCurve)?; + match (encoded_key_type.as_str(), curve.as_str()) { + ("ecdsa-sha2-nistp256", "nistp256") => {} + ("ecdsa-sha2-nistp384", "nistp384") => {} + ("ecdsa-sha2-nistp521", "nistp521") => {} + _ => { + return Err(report!(PublicKeyReadError::MismatchedCurveInfo { + key_type: encoded_key_type, + curve, + })) + } + } + + let encoded_point_bytes = ssh_buffer.get_bytes().change_context_lazy(|| { + PublicKeyReadError::CouldNotReadEcdsaPoint { + curve: curve.clone(), + } + })?; + + match curve.as_str() { + "nistp256" => { + let point = p256::EncodedPoint::from_bytes(&encoded_point_bytes).map_err( + |error| { + report!(PublicKeyReadError::InvalidEcdsaPoint { + curve: curve.clone(), + error: error.into() + }) + }, + )?; + let public = p256::PublicKey::from_encoded_point(&point) + .into_option() + .ok_or_else(|| PublicKeyReadError::InvalidEcdsaPublicValue { + curve: curve.clone(), + })?; + + Ok(PublicKey::P256(public)) + } + + "nistp384" => { + let point = p384::EncodedPoint::from_bytes(&encoded_point_bytes).map_err( + |error| { + report!(PublicKeyReadError::InvalidEcdsaPoint { + curve: curve.clone(), + error: error.into() + }) + }, + )?; + let public = p384::PublicKey::from_encoded_point(&point) + .into_option() + .ok_or_else(|| PublicKeyReadError::InvalidEcdsaPublicValue { + curve: curve.clone(), + })?; + + Ok(PublicKey::P384(public)) + } + + "nistp521" => { + let point = p521::EncodedPoint::from_bytes(&encoded_point_bytes).map_err( + |error| { + report!(PublicKeyReadError::InvalidEcdsaPoint { + curve: curve.clone(), + error: error.into() + }) + }, + )?; + let public = p521::PublicKey::from_encoded_point(&point) + .into_option() + .ok_or_else(|| PublicKeyReadError::InvalidEcdsaPublicValue { + curve: curve.clone(), + })?; + + Ok(PublicKey::P521(public)) + } + + _ => panic!( + "Should not be able to have a mismatched curve, but have {}", + curve + ), + } + } + + _ => Err(report!(PublicKeyReadError::UnrecognizedKeyType { + key_type: encoded_key_type + })), + } + } +} + +#[test] +fn short_invalid_buffer_fails_correctly() { + let buffer = Bytes::from(vec![0, 1]); + let error = PublicKey::try_from(buffer).unwrap_err(); + assert!(matches!( + error.current_context(), + &PublicKeyReadError::NoPublicKeyType + )); + + let buffer = Bytes::from(b"\x00\x00\x00\x05hippo".to_vec()); + let error = PublicKey::try_from(buffer).unwrap_err(); + assert!( + matches!(error.current_context(), &PublicKeyReadError::UnrecognizedKeyType { ref key_type } if key_type.as_str() == "hippo") + ); +} + +#[cfg(test)] +const ECDSA256_TEST_KEY: &[u8] = &[ + 0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e, + 0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70, + 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x41, 0x04, 0xd7, 0x47, 0x00, 0x93, 0x35, 0xc5, 0x88, 0xc1, + 0x67, 0xb5, 0x1d, 0x5f, 0xf1, 0x9b, 0x82, 0x1d, 0xe8, 0x37, 0x21, 0xe7, 0x89, 0xe5, 0x7c, 0x14, + 0x6a, 0xd7, 0xfe, 0x43, 0x44, 0xe7, 0x67, 0xd8, 0x05, 0x66, 0xe1, 0x96, 0x12, 0x8f, 0xc9, 0x23, + 0x1c, 0x8f, 0x25, 0x0e, 0xa7, 0xf1, 0xcd, 0x76, 0x7a, 0xea, 0xb7, 0x87, 0x24, 0x07, 0x1e, 0x72, + 0x63, 0x6b, 0x81, 0xde, 0x20, 0x81, 0xe7, 0x82, +]; + +#[cfg(test)] +const ECDSA384_TEST_KEY: &[u8] = &[ + 0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e, + 0x69, 0x73, 0x74, 0x70, 0x33, 0x38, 0x34, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70, + 0x33, 0x38, 0x34, 0x00, 0x00, 0x00, 0x61, 0x04, 0x0d, 0xa3, 0x8b, 0x42, 0x98, 0x15, 0xba, 0x0c, + 0x9b, 0xf6, 0x5e, 0xc8, 0x68, 0xc3, 0x1e, 0x44, 0xb2, 0x6f, 0x12, 0x2f, 0xc8, 0x97, 0x81, 0x23, + 0x60, 0xa0, 0xc3, 0xaf, 0xf1, 0x3f, 0x5f, 0xd6, 0xea, 0x49, 0x9c, 0xd6, 0x74, 0x34, 0xd0, 0x6a, + 0xd0, 0x34, 0xe4, 0xd8, 0x42, 0x00, 0x94, 0x61, 0x63, 0x15, 0x11, 0xb0, 0x63, 0x52, 0xcc, 0xbe, + 0xe5, 0xc2, 0x12, 0x33, 0xdc, 0x36, 0x03, 0x60, 0x6c, 0xb9, 0x11, 0xa6, 0xe4, 0x81, 0x64, 0x4a, + 0x54, 0x74, 0x2b, 0xfb, 0xbc, 0xff, 0x90, 0xe0, 0x2c, 0x00, 0xc1, 0xae, 0x99, 0x2e, 0x0f, 0xdb, + 0x50, 0xec, 0x4c, 0xe8, 0xbd, 0xf1, 0x0f, 0xdc, +]; + +#[cfg(test)] +const ECDSA521_TEST_KEY: &[u8] = &[ + 0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e, + 0x69, 0x73, 0x74, 0x70, 0x35, 0x32, 0x31, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70, + 0x35, 0x32, 0x31, 0x00, 0x00, 0x00, 0x85, 0x04, 0x01, 0x68, 0x9a, 0x37, 0xac, 0xa3, 0x16, 0x26, + 0xa4, 0xaa, 0x72, 0xe6, 0x24, 0x40, 0x4c, 0x69, 0xbf, 0x11, 0x9e, 0xcd, 0xb6, 0x63, 0x92, 0x10, + 0xa6, 0xb7, 0x6e, 0x98, 0xb4, 0xa0, 0x81, 0xc5, 0x3c, 0x88, 0xfa, 0x9b, 0x60, 0x57, 0x4c, 0x0f, + 0xba, 0x36, 0x4e, 0xc6, 0xe0, 0x3e, 0xa5, 0x86, 0x3d, 0xd3, 0xd5, 0x86, 0x96, 0xe9, 0x4a, 0x1c, + 0x0c, 0xe2, 0x70, 0xff, 0x1f, 0x79, 0x06, 0x5d, 0x52, 0x9a, 0x01, 0x2b, 0x87, 0x8e, 0xc2, 0xe9, + 0xe2, 0xb7, 0x01, 0x00, 0xa6, 0x1a, 0xf7, 0x23, 0x47, 0x6a, 0x70, 0x10, 0x09, 0x59, 0xde, 0x0a, + 0x20, 0xca, 0x2f, 0xd7, 0x5a, 0x98, 0xbd, 0xc3, 0x5b, 0xf2, 0x7b, 0x14, 0x6e, 0x6b, 0xa5, 0x93, + 0x5d, 0x3e, 0x21, 0x5c, 0x49, 0x40, 0xbf, 0x9b, 0xc0, 0x78, 0x4b, 0xb1, 0xe9, 0xc7, 0x02, 0xb1, + 0x51, 0x94, 0x1a, 0xcf, 0x88, 0x7b, 0xfe, 0xea, 0xd8, 0x55, 0x89, 0xb3, +]; + +#[test] +fn ecdsa_public_works() { + let buffer = Bytes::from(ECDSA256_TEST_KEY); + let public_key = PublicKey::try_from(buffer).unwrap(); + assert!(matches!(public_key, PublicKey::P256(_))); + + let buffer = Bytes::from(ECDSA384_TEST_KEY); + let public_key = PublicKey::try_from(buffer).unwrap(); + assert!(matches!(public_key, PublicKey::P384(_))); + + let buffer = Bytes::from(ECDSA521_TEST_KEY); + let public_key = PublicKey::try_from(buffer).unwrap(); + assert!(matches!(public_key, PublicKey::P521(_))); +} + +#[test] +fn checks_for_mismatched_curves() { + let mut raw_data = ECDSA256_TEST_KEY.to_vec(); + raw_data[32] = 0x33; + raw_data[33] = 0x38; + raw_data[34] = 0x34; + let buffer = Bytes::from(raw_data); + let error = PublicKey::try_from(buffer).unwrap_err(); + assert!( + matches!(error.current_context(), PublicKeyReadError::MismatchedCurveInfo { ref key_type, ref curve } + if key_type.as_str() == "ecdsa-sha2-nistp256" && curve.as_str() == "nistp384") + ); +} + +#[test] +fn invalid_point_errors() { + let mut raw_data = ECDSA256_TEST_KEY.to_vec(); + let _ = raw_data.pop().unwrap(); + let buffer = Bytes::from(raw_data); + let error = PublicKey::try_from(buffer).unwrap_err(); + assert!( + matches!(error.current_context(), PublicKeyReadError::CouldNotReadEcdsaPoint { ref curve } + if curve.as_str() == "nistp256") + ); + + let mut raw_data = ECDSA256_TEST_KEY.to_vec(); + raw_data[64] = 0x33; + let buffer = Bytes::from(raw_data); + let error = PublicKey::try_from(buffer).unwrap_err(); + assert!( + matches!(error.current_context(), PublicKeyReadError::InvalidEcdsaPublicValue { ref curve } + if curve.as_str() == "nistp256") + ); + + let mut raw_data = ECDSA256_TEST_KEY.to_vec(); + raw_data[39] = 0x33; + let buffer = Bytes::from(raw_data); + let error = PublicKey::try_from(buffer).unwrap_err(); + assert!( + matches!(error.current_context(), PublicKeyReadError::InvalidEcdsaPoint { ref curve, .. } + if curve.as_str() == "nistp256") + ); +} + +#[cfg(test)] +const ED25519_TEST_KEY: &[u8] = &[ + 0x00, 0x00, 0x00, 0x0b, 0x73, 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x00, + 0x00, 0x00, 0x20, 0x80, 0xe2, 0x47, 0x6a, 0x6f, 0xcb, 0x13, 0x7a, 0x0e, 0xda, 0x9b, 0x06, 0x3c, + 0x4d, 0xd7, 0x24, 0xdb, 0x31, 0x1b, 0xa9, 0xc5, 0xc3, 0x44, 0x5b, 0xda, 0xff, 0x85, 0x51, 0x15, + 0x63, 0x58, 0xd3, +]; + +#[test] +fn ed25519_public_works() { + let buffer = Bytes::from(ED25519_TEST_KEY); + let public_key = PublicKey::try_from(buffer).unwrap(); + assert!(matches!(public_key, PublicKey::Ed25519(_))); +} + +#[test] +fn shortened_ed25519_fails() { + let buffer = Bytes::from(&ED25519_TEST_KEY[0..ED25519_TEST_KEY.len() - 2]); + let error = PublicKey::try_from(buffer).unwrap_err(); + assert!(matches!( + error.current_context(), + &PublicKeyReadError::NoEd25519Data + )); +} + +#[test] +fn invalid_data_kills_ed25519_read() { + let mut raw_data = ED25519_TEST_KEY.to_vec(); + raw_data[19] = 0x00; + let buffer = Bytes::from(raw_data); + let error = PublicKey::try_from(buffer).unwrap_err(); + assert!(matches!( + error.current_context(), + &PublicKeyReadError::InvalidEd25519Data { .. } + )); +} + +#[test] +fn extraneous_data_kills_ed25519_read() { + let mut raw_data = ED25519_TEST_KEY.to_vec(); + raw_data.push(0); + let buffer = Bytes::from(raw_data); + let error = PublicKey::try_from(buffer).unwrap_err(); + assert!(matches!( + error.current_context(), + &PublicKeyReadError::ExtraneousInfo { .. } + )); +} + +#[cfg(test)] +const RSA_TEST_KEY: &[u8] = &[ + 0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2d, 0x72, 0x73, 0x61, 0x00, 0x00, 0x00, 0x03, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0xb7, 0x7e, 0xd2, 0x53, 0xf0, 0x92, 0xac, 0x06, 0x53, + 0x07, 0x8f, 0xe9, 0x89, 0xd8, 0x92, 0xd4, 0x08, 0x7e, 0xdd, 0x6b, 0xa4, 0x67, 0xd8, 0xac, 0x4a, + 0x3b, 0x8f, 0xbd, 0x2f, 0x3a, 0x19, 0x46, 0x7c, 0xa5, 0x7f, 0xc1, 0x01, 0xee, 0xe3, 0xbf, 0x9e, + 0xaf, 0xed, 0xc8, 0xbc, 0x8c, 0x30, 0x70, 0x6f, 0xf1, 0xdd, 0xb9, 0x9b, 0x4c, 0x67, 0x7b, 0x8f, + 0x7c, 0xcf, 0x85, 0x6f, 0x28, 0x5f, 0xeb, 0xe3, 0x0b, 0x7f, 0x82, 0xf5, 0xa4, 0x99, 0xc6, 0xae, + 0x1c, 0xbd, 0xd6, 0xa9, 0x34, 0xc9, 0x05, 0xfc, 0xdc, 0xe2, 0x84, 0x86, 0x69, 0xc5, 0x6b, 0x0a, + 0xf5, 0x17, 0x5f, 0x52, 0xda, 0x4a, 0xdf, 0xd9, 0x4a, 0xe2, 0x14, 0x0c, 0xba, 0x96, 0x04, 0x4e, + 0x25, 0x38, 0xd1, 0x66, 0x75, 0xf2, 0x27, 0x68, 0x1f, 0x28, 0xce, 0xa5, 0xa3, 0x22, 0x05, 0xf7, + 0x9e, 0x38, 0x70, 0xf7, 0x23, 0x65, 0xfe, 0x4e, 0x77, 0x66, 0x70, 0x16, 0x89, 0xa3, 0xa7, 0x1b, + 0xbd, 0x6d, 0x94, 0x85, 0xa1, 0x6b, 0xe8, 0xf1, 0xb9, 0xb6, 0x7f, 0x4f, 0xb4, 0x53, 0xa7, 0xfe, + 0x2d, 0x89, 0x6a, 0x6e, 0x6d, 0x63, 0x85, 0xe1, 0x00, 0x83, 0x01, 0xb0, 0x00, 0x8a, 0x30, 0xde, + 0xdc, 0x2f, 0x30, 0xbc, 0x89, 0x66, 0x2a, 0x28, 0x59, 0x31, 0xd9, 0x74, 0x9c, 0xf2, 0xf1, 0xd7, + 0x53, 0xa9, 0x7b, 0xeb, 0x97, 0xfd, 0x53, 0x13, 0x66, 0x59, 0x9d, 0x61, 0x4a, 0x72, 0xf4, 0xa9, + 0x22, 0xc8, 0xac, 0x0e, 0xd8, 0x0e, 0x4f, 0x15, 0x59, 0x9b, 0xaa, 0x96, 0xf9, 0xd5, 0x61, 0xd5, + 0x04, 0x4c, 0x09, 0x0d, 0x5a, 0x4e, 0x39, 0xd6, 0xbe, 0x16, 0x8c, 0x36, 0xe1, 0x1d, 0x59, 0x5a, + 0xa5, 0x5c, 0x50, 0x6b, 0x6f, 0x6a, 0xed, 0x63, 0x04, 0xbc, 0x42, 0xec, 0xcb, 0xea, 0x34, 0xfc, + 0x75, 0xcc, 0xd1, 0xca, 0x45, 0x66, 0xd0, 0xc9, 0x14, 0xae, 0x83, 0xd0, 0x7c, 0x0e, 0x06, 0x1d, + 0x4f, 0x15, 0x64, 0x53, 0x56, 0xdb, 0xf2, 0x49, 0x83, 0x03, 0xae, 0xda, 0xa7, 0x29, 0x7c, 0x42, + 0xbf, 0x82, 0x07, 0xbc, 0x44, 0x09, 0x15, 0x32, 0x4d, 0xc0, 0xdf, 0x8a, 0x04, 0x89, 0xd9, 0xd8, + 0xdb, 0x05, 0xa5, 0x60, 0x21, 0xed, 0xcb, 0x54, 0x74, 0x1e, 0x24, 0x06, 0x4d, 0x69, 0x93, 0x72, + 0xe8, 0x59, 0xe1, 0x93, 0x1a, 0x6e, 0x48, 0x16, 0x31, 0x38, 0x10, 0x0e, 0x0b, 0x34, 0xeb, 0x20, + 0x86, 0x9c, 0x60, 0x68, 0xaf, 0x30, 0x5e, 0x7f, 0x26, 0x37, 0xce, 0xd9, 0xc1, 0x47, 0xdf, 0x2d, + 0xba, 0x50, 0x96, 0xcf, 0xf8, 0xf5, 0xe8, 0x65, 0x26, 0x18, 0x4a, 0x88, 0xe0, 0xd8, 0xab, 0x24, + 0xde, 0x3f, 0xa9, 0x64, 0x94, 0xe3, 0xaf, 0x7b, 0x43, 0xaa, 0x72, 0x64, 0x7c, 0xef, 0xdb, 0x30, + 0x87, 0x7d, 0x70, 0xd7, 0xbe, 0x0a, 0xca, 0x79, 0xe6, 0xb8, 0x3e, 0x23, 0x37, 0x17, 0x7d, 0x0c, + 0x41, 0x3d, 0xd9, 0x92, 0xd6, 0x8c, 0x95, 0x8b, 0x63, 0x0b, 0x63, 0x49, 0x98, 0x0f, 0x1f, 0xc1, + 0x95, 0x94, 0x6f, 0x22, 0x0e, 0x47, 0x8f, 0xee, 0x12, 0xb9, 0x8e, 0x28, 0xc2, 0x94, 0xa2, 0xd4, + 0x0a, 0x79, 0x69, 0x93, 0x8a, 0x6f, 0xf4, 0xae, 0xd1, 0x85, 0x11, 0xbb, 0x6c, 0xd5, 0x41, 0x00, + 0x71, 0x9b, 0x24, 0xe4, 0x6d, 0x0a, 0x05, 0x07, 0x4c, 0x28, 0xa6, 0x88, 0x8c, 0xea, 0x74, 0x19, + 0x64, 0x26, 0x5a, 0xc8, 0x28, 0xcc, 0xdf, 0xa8, 0xea, 0xa7, 0xda, 0xec, 0x03, 0xcd, 0xcb, 0xf3, + 0xd7, 0x6b, 0xb6, 0x4a, 0xd8, 0x50, 0x44, 0x91, 0xde, 0xb2, 0x76, 0x6e, 0x85, 0x21, 0x4b, 0x2f, + 0x65, 0x57, 0x76, 0xd3, 0xd9, 0xfa, 0xd2, 0x98, 0xcb, 0x47, 0xaa, 0x33, 0x69, 0x4e, 0x83, 0x75, + 0xfe, 0x8e, 0xac, 0x0a, 0xf6, 0xb6, 0xb7, +]; + +#[test] +fn rsa_public_works() { + let buffer = Bytes::from(RSA_TEST_KEY); + let public_key = PublicKey::try_from(buffer).unwrap(); + assert!(matches!(public_key, PublicKey::Rsa(_))); +} + +#[test] +fn rsa_requires_both_constants() { + let buffer = Bytes::from(&RSA_TEST_KEY[0..RSA_TEST_KEY.len() - 2]); + let error = PublicKey::try_from(buffer).unwrap_err(); + assert!( + matches!(error.current_context(), &PublicKeyReadError::CouldNotFindRsaConstant { constant_name } if constant_name == "n") + ); + + let buffer = Bytes::from(&RSA_TEST_KEY[0..RSA_TEST_KEY.len() - 520]); + let error = PublicKey::try_from(buffer).unwrap_err(); + assert!( + matches!(error.current_context(), &PublicKeyReadError::CouldNotFindRsaConstant { constant_name } if constant_name == "e") + ); +} + +#[test] +fn extraneous_data_kills_rsa_read() { + let mut raw_data = RSA_TEST_KEY.to_vec(); + raw_data.push(0); + let buffer = Bytes::from(raw_data); + let error = PublicKey::try_from(buffer).unwrap_err(); + assert!(matches!( + error.current_context(), + &PublicKeyReadError::ExtraneousInfo { .. } + )); +} diff --git a/src/encodings/ssh/public_key_file.rs b/src/encodings/ssh/public_key_file.rs new file mode 100644 index 0000000..351de7a --- /dev/null +++ b/src/encodings/ssh/public_key_file.rs @@ -0,0 +1,204 @@ +use crate::encodings::ssh::public_key::PublicKey; +use base64::engine::{self, Engine}; +use bytes::Bytes; +use error_stack::{report, ResultExt}; +use std::path::Path; + +#[derive(Debug, thiserror::Error)] +pub enum PublicKeyLoadError { + #[error("Could not read file {file}: {error}")] + CouldNotRead { + file: String, + error: tokio::io::Error, + }, + #[error("Base64 decoding error in {file}: {error}")] + Base64 { + file: String, + error: base64::DecodeError, + }, + #[error("Could not find key type information in {file}")] + NoSshType { file: String }, + #[error("Invalid public key material found in {file}")] + InvalidKeyMaterial { file: String }, + #[error( + "Inconsistent key type found between stated type, {alleged}, and the encoded {found} key" + )] + InconsistentKeyType { + alleged: String, + found: &'static str, + }, +} + +impl PublicKey { + /// Loads one or more public keys from an OpenSSH-formatted file. + /// + /// The given file should be created by `ssh-keygen` or similar from the + /// OpenSSL suite, or from Hush tooling. Other SSH implementations may use + /// other formats that are not understood by this function. This function + /// will work on public key files where the contents have been concatenated, + /// one per line. + /// + /// Returns the key(s) loaded from the file, or an error if the file is in an + /// invalid format or contains a nonsensical crypto value. Each public key is + /// paired with the extra info that is appended at the end of the key, for + /// whatever use it might have. + pub async fn load>( + path: P, + ) -> error_stack::Result, PublicKeyLoadError> { + let mut results = vec![]; + let path = path.as_ref(); + + let all_public_key_data = tokio::fs::read_to_string(path).await.map_err(|error| { + report!(PublicKeyLoadError::CouldNotRead { + file: path.to_path_buf().display().to_string(), + error, + }) + })?; + + for public_key_data in all_public_key_data.lines() { + let (alleged_key_type, rest) = public_key_data.split_once(' ').ok_or_else(|| { + report!(PublicKeyLoadError::NoSshType { + file: path.to_path_buf().display().to_string(), + }) + })?; + + let (key_material, info) = rest.split_once(' ').unwrap_or((rest, "")); + let key_material = engine::general_purpose::STANDARD + .decode(key_material) + .map_err(|de| { + report!(PublicKeyLoadError::Base64 { + file: path.to_path_buf().display().to_string(), + error: de, + }) + })?; + let key_material = Bytes::from(key_material); + let public_key = PublicKey::try_from(key_material).change_context_lazy(|| { + PublicKeyLoadError::InvalidKeyMaterial { + file: path.to_path_buf().display().to_string(), + } + })?; + + if alleged_key_type != public_key.ssh_key_type_name() { + return Err(report!(PublicKeyLoadError::InconsistentKeyType { + alleged: alleged_key_type.to_string(), + found: public_key.ssh_key_type_name(), + })); + } + + results.push((public_key, info.to_string())); + } + + Ok(results) + } +} + +#[tokio::test] +async fn test_keys_parse() { + let mut parsed_keys = vec![]; + let test_key_directory = format!("{}/tests/ssh_keys", env!("CARGO_MANIFEST_DIR")); + let mut directory_reader = tokio::fs::read_dir(test_key_directory) + .await + .expect("can read test key directory"); + + while let Ok(Some(entry)) = directory_reader.next_entry().await { + if matches!(entry.path().extension(), Some(ext) if ext == "pub") { + let mut public_keys = PublicKey::load(entry.path()) + .await + .expect("can parse saved public key"); + + parsed_keys.append(&mut public_keys); + } + } + + assert_eq!(18, parsed_keys.len()); +} + +#[tokio::test] +async fn concatenated_keys_parse() { + use std::io::Write; + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + let ecdsa1 = tokio::fs::read(format!( + "{}/tests/ssh_keys/ecdsa1.pub", + env!("CARGO_MANIFEST_DIR") + )) + .await + .unwrap(); + named_temp.write_all(&ecdsa1).unwrap(); + let ecdsa2 = tokio::fs::read(format!( + "{}/tests/ssh_keys/ecdsa2.pub", + env!("CARGO_MANIFEST_DIR") + )) + .await + .unwrap(); + named_temp.write_all(&ecdsa2).unwrap(); + + let path = named_temp.into_temp_path(); + let parsed_keys = PublicKey::load(&path).await.unwrap(); + assert_eq!(2, parsed_keys.len()); +} + +#[tokio::test] +async fn file_errors_are_caught() { + let result = PublicKey::load("--capitan--").await; + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PublicKeyLoadError::CouldNotRead { .. }))); +} + +#[tokio::test] +async fn key_file_must_have_space() { + use std::io::Write; + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + writeln!(named_temp, "foobar").unwrap(); + let path = named_temp.into_temp_path(); + let result = PublicKey::load(path).await; + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PublicKeyLoadError::NoSshType { .. }))); +} + +#[tokio::test] +async fn key_file_must_have_valid_base64() { + use std::io::Write; + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + writeln!(named_temp, "ssh-ed25519 foobar").unwrap(); + let path = named_temp.into_temp_path(); + let result = PublicKey::load(path).await; + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PublicKeyLoadError::Base64 { .. }))); +} + +#[tokio::test] +async fn checks_for_valid_key() { + use std::io::Write; + + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + writeln!( + named_temp, + "ssh-ed25519 {}", + base64::engine::general_purpose::STANDARD.encode(b"foobar") + ) + .unwrap(); + let path = named_temp.into_temp_path(); + let result = PublicKey::load(path).await; + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PublicKeyLoadError::InvalidKeyMaterial { .. }))); +} + +#[tokio::test] +async fn mismatched_key_types_are_caught() { + use std::io::Write; + + let test_rsa_key = format!("{}/tests/ssh_keys/rsa4096a.pub", env!("CARGO_MANIFEST_DIR")); + let rsa_key_contents = tokio::fs::read_to_string(test_rsa_key).await.unwrap(); + let (_, rest) = rsa_key_contents.split_once(' ').unwrap(); + println!("rest: {:?}", rest); + let mut named_temp = tempfile::NamedTempFile::new().unwrap(); + write!(named_temp, "ssh-ed25519 ").unwrap(); + writeln!(named_temp, "{}", rest).unwrap(); + let path = named_temp.into_temp_path(); + let result = PublicKey::load(path).await; + assert!(matches!(result, Err(e) if + matches!(e.current_context(), PublicKeyLoadError::InconsistentKeyType { .. }))); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..55d5ed9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +pub mod client; +pub mod config; +pub mod crypto; +pub mod encodings; +pub mod network; +mod operational_error; +pub mod ssh; + +pub use operational_error::OperationalError; diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..5361e40 --- /dev/null +++ b/src/network.rs @@ -0,0 +1 @@ +pub mod host; diff --git a/src/network/host.rs b/src/network/host.rs new file mode 100644 index 0000000..4078f7d --- /dev/null +++ b/src/network/host.rs @@ -0,0 +1,211 @@ +use error_stack::{report, ResultExt}; +use futures::stream::{FuturesUnordered, StreamExt}; +use hickory_resolver::error::ResolveError; +use hickory_resolver::name_server::ConnectionProvider; +use hickory_resolver::AsyncResolver; +use std::collections::HashSet; +use std::fmt; +use std::net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::str::FromStr; +use thiserror::Error; +use tokio::net::TcpStream; + +pub enum Host { + IPv4(Ipv4Addr), + IPv6(Ipv6Addr), + Hostname(String), +} + +#[derive(Debug, Error)] +pub enum HostParseError { + #[error("Could not parse IPv6 address {address:?}: {error}")] + CouldNotParseIPv6 { + address: String, + error: AddrParseError, + }, + #[error("Invalid hostname {hostname:?}")] + InvalidHostname { hostname: String }, +} + +impl fmt::Display for Host { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Host::IPv4(x) => x.fmt(f), + Host::IPv6(x) => x.fmt(f), + Host::Hostname(x) => x.fmt(f), + } + } +} + +impl FromStr for Host { + type Err = HostParseError; + + fn from_str(s: &str) -> Result { + if let Ok(addr) = Ipv4Addr::from_str(s) { + return Ok(Host::IPv4(addr)); + } + + if let Ok(addr) = Ipv6Addr::from_str(s) { + return Ok(Host::IPv6(addr)); + } + + if s.starts_with('[') && s.ends_with(']') { + match s.trim_start_matches('[').trim_end_matches(']').parse() { + Ok(x) => return Ok(Host::IPv6(x)), + Err(e) => { + return Err(HostParseError::CouldNotParseIPv6 { + address: s.to_string(), + error: e, + }) + } + } + } + + if hostname_validator::is_valid(s) { + return Ok(Host::Hostname(s.to_string())); + } + + Err(HostParseError::InvalidHostname { + hostname: s.to_string(), + }) + } +} + +#[derive(Debug, Error)] +pub enum ConnectionError { + #[error("Failed to resolve host: {error}")] + ResolveError { + #[from] + error: ResolveError, + }, + #[error("No valid IP addresses found")] + NoAddresses, + #[error("Error connecting to host: {error}")] + ConnectionError { + #[from] + error: std::io::Error, + }, +} + +impl Host { + /// Resolve this host address to a set of underlying IP addresses. + /// + /// It is possible that the set of addresses provided may be empty, if the + /// address properly resolves (as in, we get a good DNS response) but there + /// are no relevant records for us to use for IPv4 or IPv6 connections. There + /// is also no guarantee that the host will have both IPv4 and IPv6 addresses, + /// so you may only see one or the other. + pub async fn resolve( + &self, + resolver: &AsyncResolver

, + ) -> Result, ResolveError> { + match self { + Host::IPv4(addr) => Ok(HashSet::from([IpAddr::V4(*addr)])), + Host::IPv6(addr) => Ok(HashSet::from([IpAddr::V6(*addr)])), + Host::Hostname(name) => { + let resolve_result = resolver.lookup_ip(name).await?; + let possibilities = resolve_result.iter().collect(); + Ok(possibilities) + } + } + } + + /// Connect to this host and port. + /// + /// This routine will attempt to connect to every address provided by the + /// resolver, and return the first successful connection. If all of the + /// connections fail, it will return the first error it receives. This routine + /// will also return an error if there are no addresses to connect to (which + /// can happen in cases in which [`Host::resolve`] would return an empty set. + pub async fn connect( + &self, + resolver: &AsyncResolver

, + port: u16, + ) -> error_stack::Result { + let addresses = self + .resolve(resolver) + .await + .map_err(ConnectionError::from) + .attach_printable_lazy(|| format!("target address {}", self))?; + + let mut connectors = FuturesUnordered::new(); + + for address in addresses.into_iter() { + let connect_future = TcpStream::connect(SocketAddr::new(address, port)); + connectors.push(connect_future); + } + + let mut error = None; + + while let Some(result) = connectors.next().await { + match result { + Err(e) if error.is_none() => error = Some(e), + Err(_) => {} + Ok(v) => return Ok(v), + } + } + + let final_error = if let Some(e) = error { + ConnectionError::ConnectionError { error: e } + } else { + ConnectionError::NoAddresses + }; + + Err(report!(final_error)).attach_printable_lazy(|| format!("target address {}", self)) + } +} + +#[test] +fn ip4_hosts_work() { + assert!( + matches!(Host::from_str("127.0.0.1"), Ok(Host::IPv4(addr)) if addr == Ipv4Addr::new(127, 0, 0, 1)) + ); +} + +#[test] +fn bare_ip6_hosts_work() { + assert!(matches!( + Host::from_str("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + Ok(Host::IPv6(_)) + )); + assert!(matches!(Host::from_str("2001:db8::1"), Ok(Host::IPv6(_)))); + assert!(matches!(Host::from_str("2001:DB8::1"), Ok(Host::IPv6(_)))); + assert!(matches!(Host::from_str("::1"), Ok(Host::IPv6(_)))); + assert!(matches!(Host::from_str("::"), Ok(Host::IPv6(_)))); +} + +#[test] +fn wrapped_ip6_hosts_work() { + assert!(matches!( + Host::from_str("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"), + Ok(Host::IPv6(_)) + )); + assert!(matches!(Host::from_str("[2001:db8::1]"), Ok(Host::IPv6(_)))); + assert!(matches!(Host::from_str("[2001:DB8::1]"), Ok(Host::IPv6(_)))); + assert!(matches!(Host::from_str("[::1]"), Ok(Host::IPv6(_)))); + assert!(matches!(Host::from_str("[::]"), Ok(Host::IPv6(_)))); +} + +#[test] +fn valid_domains_work() { + assert!(matches!( + Host::from_str("uhsure.com"), + Ok(Host::Hostname(_)) + )); + assert!(matches!( + Host::from_str("www.cs.indiana.edu"), + Ok(Host::Hostname(_)) + )); +} + +#[test] +fn invalid_inputs_fail() { + assert!(matches!( + Host::from_str("[uhsure.com]"), + Err(HostParseError::CouldNotParseIPv6 { .. }) + )); + assert!(matches!( + Host::from_str("-uhsure.com"), + Err(HostParseError::InvalidHostname { .. }) + )); +} diff --git a/src/operational_error.rs b/src/operational_error.rs new file mode 100644 index 0000000..f0df143 --- /dev/null +++ b/src/operational_error.rs @@ -0,0 +1,47 @@ +use crate::ssh::{SshKeyExchangeProcessingError, SshMessageID}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum OperationalError { + #[error("Configuration error")] + ConfigurationError, + #[error("Failed to connect to target address")] + Connection, + #[error("Failed to complete initial read: {0}")] + InitialRead(std::io::Error), + #[error("SSH banner was not formatted in UTF-8: {0}")] + BannerError(std::str::Utf8Error), + #[error("Invalid initial SSH versionling line: {line}")] + InvalidHeaderLine { line: String }, + #[error("Error writing initial banner: {0}")] + WriteBanner(std::io::Error), + #[error("Unexpected disconnect from other side.")] + Disconnect, + #[error("{message} in unexpected place.")] + UnexpectedMessage { message: SshMessageID }, + #[error("User authorization failed.")] + UserAuthFailed, + #[error("Request failed.")] + RequestFailed, + #[error("Failed to open channel.")] + OpenChannelFailure, + #[error("Other side closed connection.")] + OtherClosed, + #[error("Other side sent EOF.")] + OtherEof, + #[error("Channel failed.")] + ChannelFailure, + #[error("Error in initial handshake: {0}")] + KeyxProcessingError(#[from] SshKeyExchangeProcessingError), + #[error("Call into random number generator failed: {0}")] + RngFailure(#[from] rand::Error), + #[error("Invalid port number '{port_string}': {error}")] + InvalidPort { + port_string: String, + error: std::num::ParseIntError, + }, + #[error("Invalid hostname '{0}'")] + InvalidHostname(String), + #[error("Unable to parse host address")] + UnableToParseHostAddress, +} diff --git a/src/ssh.rs b/src/ssh.rs new file mode 100644 index 0000000..946d957 --- /dev/null +++ b/src/ssh.rs @@ -0,0 +1,8 @@ +mod channel; +mod message_ids; +mod packets; +mod preamble; + +pub use message_ids::SshMessageID; +pub use packets::SshKeyExchangeProcessingError; +pub use preamble::Preamble; diff --git a/src/ssh/channel.rs b/src/ssh/channel.rs new file mode 100644 index 0000000..62fcce4 --- /dev/null +++ b/src/ssh/channel.rs @@ -0,0 +1,428 @@ +use bytes::{BufMut, Bytes, BytesMut}; +use proptest::arbitrary::Arbitrary; +use proptest::strategy::{BoxedStrategy, Strategy}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}; +use tokio::sync::Mutex; + +const MAX_BUFFER_SIZE: usize = 64 * 1024; + +#[derive(Clone, Debug, PartialEq)] +pub struct SshPacket { + pub buffer: Bytes, +} + +pub struct SshChannel { + read_side: Mutex>, + write_side: Mutex>, + cipher_block_size: usize, + mac_length: usize, + channel_is_closed: bool, +} + +struct ReadSide { + stream: ReadHalf, + buffer: BytesMut, +} + +struct WriteSide { + stream: WriteHalf, + buffer: BytesMut, + rng: ChaCha20Rng, +} + +impl SshChannel +where + Stream: AsyncReadExt + AsyncWriteExt, + Stream: Send + Sync, + Stream: Unpin, +{ + /// Create a new SSH channel. + /// + /// SshChannels are designed to make sure the various SSH channel read / + /// write operations are cancel- and concurrency-safe. They take ownership + /// of the underlying stream once established, where "established" means + /// that we have read and written the initial SSH banners. + pub fn new(stream: Stream) -> SshChannel { + let (read_half, write_half) = tokio::io::split(stream); + + SshChannel { + read_side: Mutex::new(ReadSide { + stream: read_half, + buffer: BytesMut::with_capacity(MAX_BUFFER_SIZE), + }), + write_side: Mutex::new(WriteSide { + stream: write_half, + buffer: BytesMut::with_capacity(MAX_BUFFER_SIZE), + rng: ChaCha20Rng::from_entropy(), + }), + mac_length: 0, + cipher_block_size: 8, + channel_is_closed: false, + } + } + + /// Read an SshPacket from the wire. + /// + /// This function is cancel safe, and can be used in `select` (or similar) + /// without problems. It is also safe to be used in a multitasking setting. + /// Returns Ok(Some(...)) if a packet is found, or Ok(None) if we have + /// successfully reached the end of stream. It will also return Ok(None) + /// repeatedly after the stream is closed. + pub async fn read(&self) -> Result, std::io::Error> { + if self.channel_is_closed { + return Ok(None); + } + + let mut reader = self.read_side.lock().await; + let mut local_buffer = vec![0; 4096]; + + // First, let's try to at least get a size in there. + while reader.buffer.len() < 5 { + let amt_read = reader.stream.read(&mut local_buffer).await?; + reader.buffer.extend_from_slice(&local_buffer[0..amt_read]); + } + + let packet_size = ((reader.buffer[0] as usize) << 24) + | ((reader.buffer[1] as usize) << 16) + | ((reader.buffer[2] as usize) << 8) + | reader.buffer[3] as usize; + let padding_size = reader.buffer[4] as usize; + let total_size = 4 + 1 + packet_size + padding_size + self.mac_length; + tracing::trace!( + packet_size, + padding_size, + total_size, + "Initial packet information determined" + ); + + // Now we need to make sure that the buffer contains at least that + // many bytes. We do this transfer -- from the wire to an internal + // buffer -- to ensure cancel safety. If, at any point, this computation + // is cancelled, the lock will be released and the buffer will be in + // a reasonable place. A subsequent call should be able to pick up + // wherever we left off. + while reader.buffer.len() < total_size { + let amt_read = reader.stream.read(&mut local_buffer).await?; + reader.buffer.extend_from_slice(&local_buffer[0..amt_read]); + } + + let mut new_packet = reader.buffer.split_to(total_size); + + let _header = new_packet.split_to(5); + let payload = new_packet.split_to(total_size - padding_size - 5); + let _mac = new_packet.split_off(padding_size); + + Ok(Some(SshPacket { + buffer: payload.freeze(), + })) + } + + fn encode(&self, rng: &mut ChaCha20Rng, packet: SshPacket) -> Option { + let mut encoded_packet = BytesMut::new(); + + // Arbitrary-length padding, such that the total length of + // (packet_length || padding_length || payload || random padding) + // is a multiple of the cipher block size or 8, whichever is + // larger. There MUST be at least four bytes of padding. The + // padding SHOULD consist of random bytes. The maximum amount of + // padding is 255 bytes. + let paddingless_length = 4 + 1 + packet.buffer.len(); + // the padding we need to get to an even multiple of the cipher + // block size, naturally jumping to the cipher block size if we're + // already aligned. (this is just easier, and since we can't have + // 0 as the final padding, seems reasonable to do.) + let mut rounded_padding = + self.cipher_block_size - (paddingless_length % self.cipher_block_size); + // now we enforce the must be greater than or equal to 4 rule + if rounded_padding < 4 { + rounded_padding += self.cipher_block_size; + } + // if this ends up being > 256, then we've run into something terrible + if rounded_padding > (u8::MAX as usize) { + tracing::error!( + payload_length = packet.buffer.len(), + cipher_block_size = ?self.cipher_block_size, + computed_padding = ?rounded_padding, + "generated incoherent padding value in write" + ); + return None; + } + + encoded_packet.put_u32(packet.buffer.len() as u32); + encoded_packet.put_u8(rounded_padding as u8); + encoded_packet.put(packet.buffer); + + for _ in 0..rounded_padding { + encoded_packet.put_u8(rng.gen()); + } + + Some(encoded_packet.freeze()) + } + + /// Write an SshPacket to the wire. + /// + /// This function is cancel safe, and can be used in `select` (or similar). + /// By cancel safe, we mean that one of the following outcomes is guaranteed + /// to occur if the operation is cancelled: + /// + /// 1. The whole packet is written to the channel. + /// 2. No part of the packet is written to the channel. + /// 3. The channel is dead, and no further data can be written to it. + /// + /// Note that this means that you cannot assume that the packet is not + /// written if the operation is cancelled, it just ensures that you will + /// not be in a place in which only part of the packet has been written. + pub async fn write(&self, packet: SshPacket) -> Result<(), std::io::Error> { + let mut final_data = { self.encode(&mut self.write_side.lock().await.rng, packet) }; + + loop { + if self.channel_is_closed { + return Err(std::io::Error::from(std::io::ErrorKind::UnexpectedEof)); + } + + let mut writer = self.write_side.lock().await; + + if let Some(bytes) = final_data.take() { + if bytes.len() + writer.buffer.len() < MAX_BUFFER_SIZE { + writer.buffer.put(bytes); + } else if !bytes.is_empty() { + final_data = Some(bytes); + } + } + + let mut current_buffer = std::mem::take(&mut writer.buffer); + let _written = writer.stream.write_buf(&mut current_buffer).await?; + writer.buffer = current_buffer; + + if writer.buffer.is_empty() && final_data.is_none() { + return Ok(()); + } + } + } +} + +impl Arbitrary for SshPacket { + type Parameters = bool; + type Strategy = BoxedStrategy; + + fn arbitrary_with(start_with_real_message: Self::Parameters) -> Self::Strategy { + if start_with_real_message { + unimplemented!() + } else { + let data = proptest::collection::vec(u8::arbitrary(), 0..35000); + data.prop_map(|x| SshPacket { buffer: x.into() }).boxed() + } + } +} + +#[cfg(test)] +#[derive(Debug, Clone, PartialEq)] +enum SendData { + Left(SshPacket), + Right(SshPacket), +} + +#[cfg(test)] +impl Arbitrary for SendData { + type Parameters = ::Parameters; + type Strategy = BoxedStrategy; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + (bool::arbitrary(), SshPacket::arbitrary_with(args)) + .prop_map(|(is_left, packet)| { + if is_left { + SendData::Left(packet) + } else { + SendData::Right(packet) + } + }) + .boxed() + } +} + +proptest::proptest! { + #[test] + fn can_read_back_anything(packet in SshPacket::arbitrary()) { + let result = tokio::runtime::Runtime::new().unwrap().block_on(async { + let (left, right) = tokio::io::duplex(8192); + let leftssh = SshChannel::new(left); + let rightssh = SshChannel::new(right); + let packet_copy = packet.clone(); + + tokio::task::spawn(async move { + leftssh.write(packet_copy).await.unwrap(); + }); + rightssh.read().await.unwrap() + }); + + assert_eq!(packet, result.unwrap()); + } + + #[test] + fn sequences_send_correctly_serial(sequence in proptest::collection::vec(SendData::arbitrary(), 0..100)) { + tokio::runtime::Runtime::new().unwrap().block_on(async { + let (left, right) = tokio::io::duplex(8192); + let leftssh = SshChannel::new(left); + let rightssh = SshChannel::new(right); + + let sequence_left = sequence.clone(); + let sequence_right = sequence; + + let left_task = tokio::task::spawn(async move { + let mut errored = false; + + for item in sequence_left.into_iter() { + match item { + SendData::Left(packet) => { + let result = leftssh.write(packet).await; + errored = result.is_err(); + } + + SendData::Right(packet) => { + if let Ok(Some(item)) = leftssh.read().await { + errored = item != packet; + } else { + errored = true; + } + } + } + + if errored { + break + } + } + + !errored + }); + + let right_task = tokio::task::spawn(async move { + let mut errored = false; + + for item in sequence_right.into_iter() { + match item { + SendData::Right(packet) => { + let result = rightssh.write(packet).await; + errored = result.is_err(); + } + + SendData::Left(packet) => { + if let Ok(Some(item)) = rightssh.read().await { + errored = item != packet; + } else { + errored = true; + } + } + } + + if errored { + break + } + } + + !errored + }); + + assert!(left_task.await.unwrap()); + assert!(right_task.await.unwrap()); + }); + } + + #[test] + fn sequences_send_correctly_parallel(sequence in proptest::collection::vec(SendData::arbitrary(), 0..100)) { + use std::sync::Arc; + + tokio::runtime::Runtime::new().unwrap().block_on(async { + let (left, right) = tokio::io::duplex(8192); + let leftsshw = Arc::new(SshChannel::new(left)); + let leftsshr = leftsshw.clone(); + let rightsshw = Arc::new(SshChannel::new(right)); + let rightsshr = rightsshw.clone(); + + let sequence_left_write = sequence.clone(); + let sequence_left_read = sequence.clone(); + let sequence_right_write = sequence.clone(); + let sequence_right_read = sequence.clone(); + + let left_task_write = tokio::task::spawn(async move { + let mut errored = false; + + for item in sequence_left_write.into_iter() { + if let SendData::Left(packet) = item { + let result = leftsshw.write(packet).await; + errored = result.is_err(); + } + + if errored { + break + } + } + + !errored + }); + + let right_task_write = tokio::task::spawn(async move { + let mut errored = false; + + for item in sequence_right_write.into_iter() { + if let SendData::Right(packet) = item { + let result = rightsshw.write(packet).await; + errored = result.is_err(); + } + + if errored { + break + } + } + + !errored + }); + + let left_task_read = tokio::task::spawn(async move { + let mut errored = false; + + for item in sequence_left_read.into_iter() { + if let SendData::Right(packet) = item { + if let Ok(Some(item)) = leftsshr.read().await { + errored = item != packet; + } else { + errored = true; + } + } + + if errored { + break + } + } + + !errored + }); + + let right_task_read = tokio::task::spawn(async move { + let mut errored = false; + + for item in sequence_right_read.into_iter() { + if let SendData::Left(packet) = item { + if let Ok(Some(item)) = rightsshr.read().await { + errored = item != packet; + } else { + errored = true; + } + } + + if errored { + break + } + } + + !errored + }); + + assert!(left_task_write.await.unwrap()); + assert!(right_task_write.await.unwrap()); + assert!(left_task_read.await.unwrap()); + assert!(right_task_read.await.unwrap()); + }); + } +} diff --git a/src/ssh/message_ids.rs b/src/ssh/message_ids.rs new file mode 100644 index 0000000..dcf6f1a --- /dev/null +++ b/src/ssh/message_ids.rs @@ -0,0 +1,173 @@ +use crate::operational_error::OperationalError; +use num_enum::{FromPrimitive, IntoPrimitive}; +use proptest::arbitrary::Arbitrary; +use proptest::strategy::{BoxedStrategy, Just, Strategy}; +use std::fmt; + +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, FromPrimitive, IntoPrimitive)] +#[repr(u8)] +pub enum SshMessageID { + SSH_MSG_DISCONNECT = 1, + SSH_MSG_IGNORE = 2, + SSH_MSG_UNIMPLEMENTED = 3, + SSH_MSG_DEBUG = 4, + SSH_MSG_SERVICE_REQUEST = 5, + SSH_MSG_SERVICE_ACCEPT = 6, + SSH_MSG_KEXINIT = 20, + SSH_MSG_NEWKEYS = 21, + SSH_MSG_USERAUTH_REQUEST = 50, + SSH_MSG_USERAUTH_FAILURE = 51, + SSH_MSG_USERAUTH_SUCCESS = 52, + SSH_MSG_USERAUTH_BANNER = 53, + SSH_MSG_GLOBAL_REQUEST = 80, + SSH_MSG_REQUEST_SUCCESS = 81, + SSH_MSG_REQUEST_FAILURE = 82, + SSH_MSG_CHANNEL_OPEN = 90, + SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91, + SSH_MSG_CHANNEL_OPEN_FAILURE = 92, + SSH_MSG_CHANNEL_WINDOW_ADJUST = 93, + SSH_MSG_CHANNEL_DATA = 94, + SSH_MSG_CHANNEL_EXTENDED_DATA = 95, + SSH_MSG_CHANNEL_EOF = 96, + SSH_MSG_CHANNEL_CLOSE = 97, + SSH_MSG_CHANNEL_REQUEST = 98, + SSH_MSG_CHANNEL_SUCCESS = 99, + SSH_MSG_CHANNEL_FAILURE = 100, + #[num_enum(catch_all)] + Unknown(u8), +} + +impl fmt::Display for SshMessageID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SshMessageID::SSH_MSG_DISCONNECT => write!(f, "SSH_MSG_DISCONNECT"), + SshMessageID::SSH_MSG_IGNORE => write!(f, "SSH_MSG_IGNORE"), + SshMessageID::SSH_MSG_UNIMPLEMENTED => write!(f, "SSH_MSG_UNIMPLEMENTED"), + SshMessageID::SSH_MSG_DEBUG => write!(f, "SSH_MSG_DEBUG"), + SshMessageID::SSH_MSG_SERVICE_REQUEST => write!(f, "SSH_MSG_SERVICE_REQUEST"), + SshMessageID::SSH_MSG_SERVICE_ACCEPT => write!(f, "SSH_MSG_SERVICE_ACCEPT"), + SshMessageID::SSH_MSG_KEXINIT => write!(f, "SSH_MSG_KEXINIT"), + SshMessageID::SSH_MSG_NEWKEYS => write!(f, "SSH_MSG_NEWKEYS"), + SshMessageID::SSH_MSG_USERAUTH_REQUEST => write!(f, "SSH_MSG_USERAUTH_REQUEST"), + SshMessageID::SSH_MSG_USERAUTH_FAILURE => write!(f, "SSH_MSG_USERAUTH_FAILURE"), + SshMessageID::SSH_MSG_USERAUTH_SUCCESS => write!(f, "SSH_MSG_USERAUTH_SUCCESS"), + SshMessageID::SSH_MSG_USERAUTH_BANNER => write!(f, "SSH_MSG_USERAUTH_BANNER"), + SshMessageID::SSH_MSG_GLOBAL_REQUEST => write!(f, "SSH_MSG_GLOBAL_REQUEST"), + SshMessageID::SSH_MSG_REQUEST_SUCCESS => write!(f, "SSH_MSG_REQUEST_SUCCESS"), + SshMessageID::SSH_MSG_REQUEST_FAILURE => write!(f, "SSH_MSG_REQUEST_FAILURE"), + SshMessageID::SSH_MSG_CHANNEL_OPEN => write!(f, "SSH_MSG_CHANNEL_OPEN"), + SshMessageID::SSH_MSG_CHANNEL_OPEN_CONFIRMATION => { + write!(f, "SSH_MSG_CHANNEL_OPEN_CONFIRMATION") + } + SshMessageID::SSH_MSG_CHANNEL_OPEN_FAILURE => write!(f, "SSH_MSG_CHANNEL_OPEN_FAILURE"), + SshMessageID::SSH_MSG_CHANNEL_WINDOW_ADJUST => { + write!(f, "SSH_MSG_CHANNEL_WINDOW_ADJUST") + } + SshMessageID::SSH_MSG_CHANNEL_DATA => write!(f, "SSH_MSG_CHANNEL_DATA"), + SshMessageID::SSH_MSG_CHANNEL_EXTENDED_DATA => { + write!(f, "SSH_MSG_CHANNEL_EXTENDED_DATA") + } + SshMessageID::SSH_MSG_CHANNEL_EOF => write!(f, "SSH_MSG_CHANNEL_EOF"), + SshMessageID::SSH_MSG_CHANNEL_CLOSE => write!(f, "SSH_MSG_CHANNEL_CLOSE"), + SshMessageID::SSH_MSG_CHANNEL_REQUEST => write!(f, "SSH_MSG_CHANNEL_REQUEST"), + SshMessageID::SSH_MSG_CHANNEL_SUCCESS => write!(f, "SSH_MSG_CHANNEL_SUCCESS"), + SshMessageID::SSH_MSG_CHANNEL_FAILURE => write!(f, "SSH_MSG_CHANNEL_FAILURE"), + SshMessageID::Unknown(x) => write!(f, "SSH_MSG_UNKNOWN{}", x), + } + } +} + +#[test] +fn no_duplicate_messages() { + let mut found = std::collections::HashSet::new(); + + for i in u8::MIN..=u8::MAX { + let id = SshMessageID::from_primitive(i); + let display = id.to_string(); + + assert!(!found.contains(&display)); + found.insert(display); + } +} + +impl From for OperationalError { + fn from(message: SshMessageID) -> Self { + match message { + SshMessageID::SSH_MSG_DISCONNECT => OperationalError::Disconnect, + SshMessageID::SSH_MSG_USERAUTH_FAILURE => OperationalError::UserAuthFailed, + SshMessageID::SSH_MSG_REQUEST_FAILURE => OperationalError::RequestFailed, + SshMessageID::SSH_MSG_CHANNEL_OPEN_FAILURE => OperationalError::OpenChannelFailure, + SshMessageID::SSH_MSG_CHANNEL_EOF => OperationalError::OtherEof, + SshMessageID::SSH_MSG_CHANNEL_CLOSE => OperationalError::OtherClosed, + SshMessageID::SSH_MSG_CHANNEL_FAILURE => OperationalError::ChannelFailure, + + _ => OperationalError::UnexpectedMessage { message }, + } + } +} + +impl TryFrom for SshMessageID { + type Error = OperationalError; + + fn try_from(value: OperationalError) -> Result { + match value { + OperationalError::Disconnect => Ok(SshMessageID::SSH_MSG_DISCONNECT), + OperationalError::UserAuthFailed => Ok(SshMessageID::SSH_MSG_USERAUTH_FAILURE), + OperationalError::RequestFailed => Ok(SshMessageID::SSH_MSG_REQUEST_FAILURE), + OperationalError::OpenChannelFailure => Ok(SshMessageID::SSH_MSG_CHANNEL_OPEN_FAILURE), + OperationalError::OtherEof => Ok(SshMessageID::SSH_MSG_CHANNEL_EOF), + OperationalError::OtherClosed => Ok(SshMessageID::SSH_MSG_CHANNEL_CLOSE), + OperationalError::ChannelFailure => Ok(SshMessageID::SSH_MSG_CHANNEL_FAILURE), + OperationalError::UnexpectedMessage { message } => Ok(message), + + _ => Err(value), + } + } +} + +impl Arbitrary for SshMessageID { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + proptest::prop_oneof![ + Just(SshMessageID::SSH_MSG_DISCONNECT), + Just(SshMessageID::SSH_MSG_IGNORE), + Just(SshMessageID::SSH_MSG_UNIMPLEMENTED), + Just(SshMessageID::SSH_MSG_DEBUG), + Just(SshMessageID::SSH_MSG_SERVICE_REQUEST), + Just(SshMessageID::SSH_MSG_SERVICE_ACCEPT), + Just(SshMessageID::SSH_MSG_KEXINIT), + Just(SshMessageID::SSH_MSG_NEWKEYS), + Just(SshMessageID::SSH_MSG_USERAUTH_REQUEST), + Just(SshMessageID::SSH_MSG_USERAUTH_FAILURE), + Just(SshMessageID::SSH_MSG_USERAUTH_SUCCESS), + Just(SshMessageID::SSH_MSG_USERAUTH_BANNER), + Just(SshMessageID::SSH_MSG_GLOBAL_REQUEST), + Just(SshMessageID::SSH_MSG_REQUEST_SUCCESS), + Just(SshMessageID::SSH_MSG_REQUEST_FAILURE), + Just(SshMessageID::SSH_MSG_CHANNEL_OPEN), + Just(SshMessageID::SSH_MSG_CHANNEL_OPEN_CONFIRMATION), + Just(SshMessageID::SSH_MSG_CHANNEL_OPEN_FAILURE), + Just(SshMessageID::SSH_MSG_CHANNEL_WINDOW_ADJUST), + Just(SshMessageID::SSH_MSG_CHANNEL_DATA), + Just(SshMessageID::SSH_MSG_CHANNEL_EXTENDED_DATA), + Just(SshMessageID::SSH_MSG_CHANNEL_EOF), + Just(SshMessageID::SSH_MSG_CHANNEL_CLOSE), + Just(SshMessageID::SSH_MSG_CHANNEL_REQUEST), + Just(SshMessageID::SSH_MSG_CHANNEL_SUCCESS), + Just(SshMessageID::SSH_MSG_CHANNEL_FAILURE), + ] + .boxed() + } +} + +proptest::proptest! { + #[test] + fn error_encodings_invert(message in SshMessageID::arbitrary()) { + let error_version = OperationalError::from(message); + let back_to_message = SshMessageID::try_from(error_version).unwrap(); + assert_eq!(message, back_to_message); + } +} diff --git a/src/ssh/packets.rs b/src/ssh/packets.rs new file mode 100644 index 0000000..e772fe6 --- /dev/null +++ b/src/ssh/packets.rs @@ -0,0 +1,3 @@ +mod key_exchange; + +pub use key_exchange::SshKeyExchangeProcessingError; diff --git a/src/ssh/packets/key_exchange.rs b/src/ssh/packets/key_exchange.rs new file mode 100644 index 0000000..62be714 --- /dev/null +++ b/src/ssh/packets/key_exchange.rs @@ -0,0 +1,332 @@ +use crate::config::ClientConnectionOpts; +use crate::ssh::channel::SshPacket; +use crate::ssh::message_ids::SshMessageID; +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use itertools::Itertools; +use proptest::arbitrary::Arbitrary; +use proptest::strategy::{BoxedStrategy, Strategy}; +use rand::{CryptoRng, Rng, SeedableRng}; +use std::string::FromUtf8Error; +use thiserror::Error; + +#[derive(Clone, Debug, PartialEq)] +pub struct SshKeyExchange { + cookie: [u8; 16], + keyx_algorithms: Vec, + server_host_key_algorithms: Vec, + encryption_algorithms_client_to_server: Vec, + encryption_algorithms_server_to_client: Vec, + mac_algorithms_client_to_server: Vec, + mac_algorithms_server_to_client: Vec, + compression_algorithms_client_to_server: Vec, + compression_algorithms_server_to_client: Vec, + languages_client_to_server: Vec, + languages_server_to_client: Vec, + first_kex_packet_follows: bool, +} + +#[derive(Debug, Error)] +pub enum SshKeyExchangeProcessingError { + #[error("Message not appropriately tagged as SSH_MSG_KEXINIT")] + TaggedWrong, + #[error("Initial key exchange message was too short.")] + TooShort, + #[error("Invalid string encoding for name-list (not ASCII)")] + NotAscii, + #[error("Invalid conversion (from ASCII to UTF-8??): {0}")] + NotUtf8(FromUtf8Error), + #[error("Extraneous data at the end of key exchange message")] + ExtraneousData, + #[error("Received invalid reserved word ({0} != 0)")] + InvalidReservedWord(u32), +} + +impl TryFrom for SshKeyExchange { + type Error = SshKeyExchangeProcessingError; + + fn try_from(mut value: SshPacket) -> Result { + if SshMessageID::from(value.buffer.get_u8()) != SshMessageID::SSH_MSG_KEXINIT { + return Err(SshKeyExchangeProcessingError::TaggedWrong); + } + + let mut cookie = [0; 16]; + check_length(&mut value.buffer, 16)?; + value.buffer.copy_to_slice(&mut cookie); + + let keyx_algorithms = name_list(&mut value.buffer)?; + let server_host_key_algorithms = name_list(&mut value.buffer)?; + let encryption_algorithms_client_to_server = name_list(&mut value.buffer)?; + let encryption_algorithms_server_to_client = name_list(&mut value.buffer)?; + let mac_algorithms_client_to_server = name_list(&mut value.buffer)?; + let mac_algorithms_server_to_client = name_list(&mut value.buffer)?; + let compression_algorithms_client_to_server = name_list(&mut value.buffer)?; + let compression_algorithms_server_to_client = name_list(&mut value.buffer)?; + let languages_client_to_server = name_list(&mut value.buffer)?; + let languages_server_to_client = name_list(&mut value.buffer)?; + check_length(&mut value.buffer, 5)?; + let first_kex_packet_follows = value.buffer.get_u8() != 0; + let reserved = value.buffer.get_u32(); + if reserved != 0 { + return Err(SshKeyExchangeProcessingError::InvalidReservedWord(reserved)); + } + + if value.buffer.remaining() > 0 { + return Err(SshKeyExchangeProcessingError::ExtraneousData); + } + + Ok(SshKeyExchange { + cookie, + keyx_algorithms, + server_host_key_algorithms, + encryption_algorithms_client_to_server, + encryption_algorithms_server_to_client, + mac_algorithms_client_to_server, + mac_algorithms_server_to_client, + compression_algorithms_client_to_server, + compression_algorithms_server_to_client, + languages_client_to_server, + languages_server_to_client, + first_kex_packet_follows, + }) + } +} + +impl From for SshPacket { + fn from(value: SshKeyExchange) -> Self { + let mut buffer = BytesMut::new(); + + let put_options = |buffer: &mut BytesMut, vals: Vec| { + let mut merged = String::new(); + #[allow(unstable_name_collisions)] + let comma_sepped = vals.into_iter().intersperse(String::from(",")); + merged.extend(comma_sepped); + let bytes = merged.as_bytes(); + buffer.put_u32(bytes.len() as u32); + buffer.put_slice(bytes); + }; + + buffer.put_u8(SshMessageID::SSH_MSG_KEXINIT.into()); + buffer.put_slice(&value.cookie); + put_options(&mut buffer, value.keyx_algorithms); + put_options(&mut buffer, value.server_host_key_algorithms); + put_options(&mut buffer, value.encryption_algorithms_client_to_server); + put_options(&mut buffer, value.encryption_algorithms_server_to_client); + put_options(&mut buffer, value.mac_algorithms_client_to_server); + put_options(&mut buffer, value.mac_algorithms_server_to_client); + put_options(&mut buffer, value.compression_algorithms_client_to_server); + put_options(&mut buffer, value.compression_algorithms_server_to_client); + put_options(&mut buffer, value.languages_client_to_server); + put_options(&mut buffer, value.languages_server_to_client); + buffer.put_u8(value.first_kex_packet_follows as u8); + buffer.put_u32(0); + + SshPacket { + buffer: buffer.freeze(), + } + } +} + +impl SshKeyExchange { + /// Create a new SshKeyExchange message for this client or server based + /// on the given connection options. + /// + /// This function takes a random number generator because it needs to + /// seed the message with a random cookie, but is otherwise deterministic. + /// It will fail only in the case that the underlying random number + /// generator fails, and return exactly that error. + pub fn new(rng: &mut R, value: ClientConnectionOpts) -> Result + where + R: CryptoRng + Rng, + { + let mut result = SshKeyExchange { + cookie: [0; 16], + keyx_algorithms: value + .key_exchange_algorithms + .iter() + .map(|x| x.to_string()) + .collect(), + server_host_key_algorithms: value + .server_host_key_algorithms + .iter() + .map(|x| x.to_string()) + .collect(), + encryption_algorithms_client_to_server: value + .encryption_algorithms + .iter() + .map(|x| x.to_string()) + .collect(), + encryption_algorithms_server_to_client: value + .encryption_algorithms + .iter() + .map(|x| x.to_string()) + .collect(), + mac_algorithms_client_to_server: value + .mac_algorithms + .iter() + .map(|x| x.to_string()) + .collect(), + mac_algorithms_server_to_client: value + .mac_algorithms + .iter() + .map(|x| x.to_string()) + .collect(), + compression_algorithms_client_to_server: value + .compression_algorithms + .iter() + .map(|x| x.to_string()) + .collect(), + compression_algorithms_server_to_client: value + .compression_algorithms + .iter() + .map(|x| x.to_string()) + .collect(), + languages_client_to_server: value.languages.to_vec(), + languages_server_to_client: value.languages.to_vec(), + first_kex_packet_follows: value.predict.is_some(), + }; + + rng.try_fill(&mut result.cookie)?; + + Ok(result) + } +} + +impl Arbitrary for SshKeyExchange { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_: Self::Parameters) -> BoxedStrategy { + let client_config = ClientConnectionOpts::arbitrary(); + let seed = <[u8; 32]>::arbitrary(); + + (client_config, seed) + .prop_map(|(config, seed)| { + let mut rng = rand_chacha::ChaCha20Rng::from_seed(seed); + SshKeyExchange::new(&mut rng, config).unwrap() + }) + .boxed() + } +} + +proptest::proptest! { + #[test] + fn valid_kex_messages_parse(kex in SshKeyExchange::arbitrary()) { + let as_packet: SshPacket = kex.clone().try_into().expect("can generate packet"); + let as_message = as_packet.try_into().expect("can regenerate message"); + assert_eq!(kex, as_message); + } +} + +fn check_length(buffer: &mut Bytes, length: usize) -> Result<(), SshKeyExchangeProcessingError> { + if buffer.remaining() < length { + Err(SshKeyExchangeProcessingError::TooShort) + } else { + Ok(()) + } +} + +fn name_list(buffer: &mut Bytes) -> Result, SshKeyExchangeProcessingError> { + check_length(buffer, 4)?; + let list_length = buffer.get_u32() as usize; + + if list_length == 0 { + return Ok(vec![]); + } + + check_length(buffer, list_length)?; + let mut raw_bytes = vec![0u8; list_length]; + buffer.copy_to_slice(&mut raw_bytes); + if !raw_bytes.iter().all(|c| c.is_ascii()) { + return Err(SshKeyExchangeProcessingError::NotAscii); + } + + let mut result = Vec::new(); + for split in raw_bytes.split(|b| char::from(*b) == ',') { + let str = + String::from_utf8(split.to_vec()).map_err(SshKeyExchangeProcessingError::NotUtf8)?; + result.push(str); + } + + Ok(result) +} + +#[cfg(test)] +fn standard_kex_message() -> SshPacket { + let seed = [0u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(seed); + let config = ClientConnectionOpts::default(); + let message = SshKeyExchange::new(&mut rng, config).expect("default settings work"); + message.try_into().expect("default settings serialize") +} + +#[test] +fn can_see_bad_tag() { + let standard = standard_kex_message(); + let mut buffer = standard.buffer.to_vec(); + buffer[0] += 1; + let bad = SshPacket { + buffer: buffer.into(), + }; + assert!(matches!( + SshKeyExchange::try_from(bad), + Err(SshKeyExchangeProcessingError::TaggedWrong) + )); +} + +#[test] +fn checks_for_extraneous_data() { + let standard = standard_kex_message(); + let mut buffer = standard.buffer.to_vec(); + buffer.push(3); + let bad = SshPacket { + buffer: buffer.into(), + }; + assert!(matches!( + SshKeyExchange::try_from(bad), + Err(SshKeyExchangeProcessingError::ExtraneousData) + )); +} + +#[test] +fn checks_for_short_packets() { + let standard = standard_kex_message(); + let mut buffer = standard.buffer.to_vec(); + let _ = buffer.pop(); + let bad = SshPacket { + buffer: buffer.into(), + }; + assert!(matches!( + SshKeyExchange::try_from(bad), + Err(SshKeyExchangeProcessingError::TooShort) + )); +} + +#[test] +fn checks_for_invalid_data() { + let standard = standard_kex_message(); + let mut buffer = standard.buffer.to_vec(); + buffer[22] = 0xc3; + buffer[23] = 0x28; + let bad = SshPacket { + buffer: buffer.into(), + }; + assert!(matches!( + SshKeyExchange::try_from(bad), + Err(SshKeyExchangeProcessingError::NotAscii) + )); +} + +#[test] +fn checks_for_bad_reserved_word() { + let standard = standard_kex_message(); + let mut buffer = standard.buffer.to_vec(); + let _ = buffer.pop(); + buffer.push(1); + let bad = SshPacket { + buffer: buffer.into(), + }; + assert!(matches!( + SshKeyExchange::try_from(bad), + Err(SshKeyExchangeProcessingError::InvalidReservedWord(1)) + )); +} diff --git a/src/ssh/preamble.rs b/src/ssh/preamble.rs new file mode 100644 index 0000000..ad830bf --- /dev/null +++ b/src/ssh/preamble.rs @@ -0,0 +1,392 @@ +use error_stack::{report, ResultExt}; +use proptest::arbitrary::Arbitrary; +use proptest::strategy::{BoxedStrategy, Strategy}; +use thiserror::Error; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +#[derive(Debug, PartialEq)] +pub struct Preamble { + preamble: String, + software_name: String, + software_version: String, + commentary: String, +} + +impl Arbitrary for Preamble { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + let name = proptest::string::string_regex("[[:alpha:]][[:alnum:]]{0,32}").unwrap(); + let soft_major = u8::arbitrary(); + let soft_minor = u8::arbitrary(); + let soft_patch = proptest::option::of(u8::arbitrary()); + let commentary = proptest::option::of( + proptest::string::string_regex("[[:alnum:]][[[:alnum:]][[:blank:]][[:punct:]]]{0,64}") + .unwrap(), + ); + + (name, soft_major, soft_minor, soft_patch, commentary) + .prop_map(|(name, major, minor, patch, commentary)| Preamble { + preamble: String::new(), + software_name: name, + software_version: if let Some(patch) = patch { + format!("{}.{}.{}", major, minor, patch) + } else { + format!("{}.{}", major, minor) + }, + commentary: commentary.unwrap_or_default(), + }) + .boxed() + } +} + +impl Default for Preamble { + fn default() -> Self { + Preamble { + preamble: String::new(), + software_name: env!("CARGO_PKG_NAME").to_string(), + software_version: env!("CARGO_PKG_VERSION").to_string(), + commentary: String::new(), + } + } +} + +#[derive(Debug, Error)] +pub enum PreambleReadError { + #[error("Reading from the input stream failed")] + Read, + #[error("Illegal version number, expected '2.0' (saw characters {0})")] + IllegalVersion(String), + #[error("No dash found after seeing SSH version number")] + NoDashAfterVersion, + #[error("Illegal character in SSH software name")] + IllegalSoftwareNameChar, + #[error("Protocol error in preamble: No line feed for carriage return")] + NoLineFeedForCarriage, + #[error("Missing the final newline in the preamble")] + MissingFinalNewline, + #[error("Illegal UTF-8 in software name")] + InvalidSoftwareName, +} + +#[derive(Debug, Error)] +pub enum PreambleWriteError { + #[error("Could not write preamble to socket")] + Write, +} + +#[derive(Debug)] +enum PreambleState { + StartOfLine, + Preamble, + CarriageReturn, + InitialS, + SecondS, + InitialH, + InitialDash, + Version2, + VersionDot, + Version0, + VersionDash, + SoftwareName, + SoftwareVersion, + Commentary, + FinalCarriageReturn, +} + +impl Preamble { + /// Read an SSH preamble from the given read channel. + /// + /// Will fail if the underlying read channel fails, or if the preamble does not + /// meet the formatting requirements of the RFC. + pub async fn read( + connection: &mut R, + ) -> error_stack::Result { + let mut preamble = String::new(); + let mut software_name_bytes = Vec::new(); + let mut software_version = String::new(); + let mut commentary = String::new(); + let mut state = PreambleState::StartOfLine; + + loop { + let next_byte = connection + .read_u8() + .await + .change_context(PreambleReadError::Read)?; + let next_char = char::from(next_byte); + + tracing::trace!(?next_char, ?state, "processing next preamble character"); + match state { + PreambleState::StartOfLine => match next_char { + 'S' => state = PreambleState::InitialS, + _ => { + preamble.push(next_char); + state = PreambleState::Preamble; + } + }, + + PreambleState::Preamble => match next_char { + '\r' => state = PreambleState::CarriageReturn, + _ => preamble.push(next_char), + }, + + PreambleState::CarriageReturn => match next_char { + '\n' => state = PreambleState::StartOfLine, + _ => return Err(report!(PreambleReadError::NoLineFeedForCarriage)), + }, + + PreambleState::InitialS => match next_char { + 'S' => state = PreambleState::SecondS, + _ => { + preamble.push('S'); + preamble.push(next_char); + state = PreambleState::Preamble; + } + }, + + PreambleState::SecondS => match next_char { + 'H' => state = PreambleState::InitialH, + _ => { + preamble.push_str("SS"); + preamble.push(next_char); + state = PreambleState::Preamble; + } + }, + + PreambleState::InitialH => match next_char { + '-' => state = PreambleState::InitialDash, + _ => { + preamble.push_str("SSH"); + preamble.push(next_char); + state = PreambleState::InitialDash; + } + }, + + PreambleState::InitialDash => match next_char { + '2' => state = PreambleState::Version2, + _ => { + return Err(report!(PreambleReadError::IllegalVersion(String::from( + next_char + )))) + } + }, + + PreambleState::Version2 => match next_char { + '.' => state = PreambleState::VersionDot, + _ => { + return Err(report!(PreambleReadError::IllegalVersion(format!( + "2{}", + next_char + )))) + } + }, + + PreambleState::VersionDot => match next_char { + '0' => state = PreambleState::Version0, + _ => { + return Err(report!(PreambleReadError::IllegalVersion(format!( + "2.{}", + next_char + )))) + } + }, + + PreambleState::Version0 => match next_char { + '-' => state = PreambleState::VersionDash, + _ => return Err(report!(PreambleReadError::NoDashAfterVersion)), + }, + + PreambleState::VersionDash => { + software_name_bytes.push(next_byte); + state = PreambleState::SoftwareName; + } + + PreambleState::SoftwareName => match next_char { + '_' => state = PreambleState::SoftwareVersion, + x if x == '-' || x.is_ascii_whitespace() => { + return Err(report!(PreambleReadError::IllegalSoftwareNameChar)) + } + _ => software_name_bytes.push(next_byte), + }, + + PreambleState::SoftwareVersion => match next_char { + ' ' => state = PreambleState::Commentary, + '\r' => state = PreambleState::FinalCarriageReturn, + '_' => state = PreambleState::SoftwareVersion, + x if x == '-' || x.is_ascii_whitespace() => { + return Err(report!(PreambleReadError::IllegalSoftwareNameChar)) + .attach_printable_lazy(|| { + format!("saw {:?} / {}", next_char, next_byte) + })? + } + _ => software_version.push(next_char), + }, + + PreambleState::FinalCarriageReturn => match next_char { + '\n' => break, + _ => return Err(report!(PreambleReadError::MissingFinalNewline)), + }, + + PreambleState::Commentary => match next_char { + '\r' => state = PreambleState::FinalCarriageReturn, + _ => commentary.push(next_char), + }, + } + } + + let software_name = String::from_utf8(software_name_bytes) + .change_context(PreambleReadError::InvalidSoftwareName)?; + + Ok(Preamble { + preamble, + software_name, + software_version, + commentary, + }) + } + + // let mut read_buffer = vec![0; 4096]; + // let mut pre_message = String::new(); + // let protocol_version; + // let software_name; + // let software_version; + // let commentary; + // let mut prefix = String::new(); + // + // + // 'outer: loop { + // let read_length = connection + // .read(&mut read_buffer) + // .await + // .change_context(PreambleReadError::InitialRead)?; + // let string_version = std::str::from_utf8(&read_buffer[0..read_length]) + // .change_context(PreambleReadError::BannerError)?; + // + // prefix.push_str(string_version); + // let ends_with_newline = prefix.ends_with("\r\n"); + // + // let new_prefix = if ends_with_newline { + // // we are cleanly reading up to a \r\n, so our new prefix after + // // this loop is empty + // String::new() + // } else if let Some((correct_bits, leftover)) = prefix.rsplit_once("\r\n") { + // // there's some dangling bits in this read, so we'll cut this string + // // at the final "\r\n" and then remember to use the leftover as the + // // new prefix at the end of this loop + // let result = leftover.to_string(); + // prefix = correct_bits.to_string(); + // result + // } else { + // // there's no "\r\n", so we don't have a full line yet, so keep reading + // continue; + // }; + // + // for line in prefix.lines() { + // if line.starts_with("SSH") { + // let (_, interesting_bits) = line + // .split_once('-') + // .ok_or_else(|| + // report!(PreambleReadError::InvalidHeaderLine { + // reason: "could not find dash after SSH", + // line: line.to_string(), + // }))?; + // + // let (protoversion, other_bits) = interesting_bits + // .split_once('-') + // .ok_or_else(|| report!(PreambleReadError::InvalidHeaderLine { + // reason: "could not find dash after protocol version", + // line: line.to_string(), + // }))?; + // + // let (softwarever, comment) = match other_bits.split_once(' ') { + // Some((s, c)) => (s, c), + // None => (other_bits, ""), + // }; + // + // let (software_name_str, software_version_str) = softwarever + // .split_once('_') + // .ok_or_else(|| report!(PreambleReadError::InvalidHeaderLine { + // reason: "could not find underscore between software name and version", + // line: line.to_string(), + // }))?; + // + // software_name = software_name_str.to_string(); + // software_version = software_version_str.to_string(); + // protocol_version = protoversion.to_string(); + // commentary = comment.to_string(); + // break 'outer; + // } else { + // pre_message.push_str(line); + // pre_message.push('\n'); + // } + // } + // + // prefix = new_prefix; + // } + // + // tracing::info!( + // ?protocol_version, + // ?software_version, + // ?commentary, + // "Got server information" + // ); + // + // Ok(Preamble { + // protocol_version, + // software_name, + // software_version, + // commentary, + // }) + // } + + /// Write a preamble to the given network socket. + pub async fn write( + &self, + connection: &mut W, + ) -> error_stack::Result<(), PreambleWriteError> { + let comment = if self.commentary.is_empty() { + self.commentary.clone() + } else { + format!(" {}", self.commentary) + }; + let output = format!( + "SSH-2.0-{}_{}{}\r\n", + self.software_name, self.software_version, comment + ); + connection + .write_all(output.as_bytes()) + .await + .change_context(PreambleWriteError::Write) + } +} + +proptest::proptest! { + #[test] + fn preamble_roundtrips(preamble in Preamble::arbitrary()) { + let read_version = tokio::runtime::Runtime::new().unwrap().block_on(async { + let (mut writer, mut reader) = tokio::io::duplex(4096); + preamble.write(&mut writer).await.unwrap(); + Preamble::read(&mut reader).await.unwrap() + }); + + assert_eq!(preamble, read_version); + } + + #[test] + fn preamble_read_is_thrifty(preamble in Preamble::arbitrary(), b in u8::arbitrary()) { + let (read_version, next) = tokio::runtime::Runtime::new().unwrap().block_on(async { + let (mut writer, mut reader) = tokio::io::duplex(4096); + preamble.write(&mut writer).await.unwrap(); + writer.write_u8(b).await.unwrap(); + writer.flush().await.unwrap(); + drop(writer); + let preamble = Preamble::read(&mut reader).await.unwrap(); + let next = reader.read_u8().await.unwrap(); + (preamble, next) + }); + + assert_eq!(preamble, read_version); + assert_eq!(b, next); + } +} diff --git a/tests/all_keys.toml b/tests/all_keys.toml new file mode 100644 index 0000000..009d7a9 --- /dev/null +++ b/tests/all_keys.toml @@ -0,0 +1,113 @@ +[runtime] +worker_threads = 4 +blocking_threads = 8 + +[logging] +level = "DEBUG" +include_filename = true +include_lineno = true +include_thread_ids = true +include_thread_names = true +mode = "Compact" +target = "stdout" + +[keys] + +[keys.ecdsa_clear1] +public = "tests/ssh_keys/ecdsa1.pub" +private = "tests/ssh_keys/ecdsa1" + +[keys.ecdsa_clear2] +public = "tests/ssh_keys/ecdsa2.pub" +private = "tests/ssh_keys/ecdsa2" + +[keys.ecdsa_big1] +public = "tests/ssh_keys/ecdsa384a.pub" +private = "tests/ssh_keys/ecdsa384a" + +[keys.ecdsa_big2] +public = "tests/ssh_keys/ecdsa384b.pub" +private = "tests/ssh_keys/ecdsa384b" + +[keys.ecdsa_biggest1] +public = "tests/ssh_keys/ecdsa384a.pub" +private = "tests/ssh_keys/ecdsa384a" + +[keys.ecdsa_biggest2] +public = "tests/ssh_keys/ecdsa521b.pub" +private = "tests/ssh_keys/ecdsa521b" + +[keys.ed25519_clear1] +public = "tests/ssh_keys/ed25519a.pub" +private = "tests/ssh_keys/ed25519a" + +[keys.ed25519_clear2] +public = "tests/ssh_keys/ed25519b.pub" +private = "tests/ssh_keys/ed25519b" + +[keys.ed25519_pass_here] +public = "tests/ssh_keys/ed25519a.pub" +private = "tests/ssh_keys/ed25519a" +password = "hush" + +[keys.ed25519_pass_on_load] +public = "tests/ssh_keys/ed25519b.pub" +private = "tests/ssh_keys/ed25519b" + +[keys.rsa_reasonable1] +public = "tests/ssh_keys/rsa4096a.pub" +private = "tests/ssh_keys/rsa4096a" + +[keys.rsa_reasonable2] +public = "tests/ssh_keys/rsa4096b.pub" +private = "tests/ssh_keys/rsa4096b" + +[keys.rsa_big1] +public = "tests/ssh_keys/rsa7680a.pub" +private = "tests/ssh_keys/rsa7680a" + +[keys.rsa_big2] +public = "tests/ssh_keys/rsa7680b.pub" +private = "tests/ssh_keys/rsa7680b" + +[keys.rsa_extra1] +public = "tests/ssh_keys/rsa8192a.pub" +private = "tests/ssh_keys/rsa8192a" + +[keys.rsa_extra2] +public = "tests/ssh_keys/rsa8192b.pub" +private = "tests/ssh_keys/rsa8192b" + +[keys.rsa_crazy1] +public = "tests/ssh_keys/rsa15360a.pub" +private = "tests/ssh_keys/rsa15360a" + +[keys.rsa_crazy2] +public = "tests/ssh_keys/rsa15360b.pub" +private = "tests/ssh_keys/rsa15360b" + +[defaults] +key_exchange_algorithms = [ "curve25519-sha256" ] +server_host_algorithms = [ "ed25519" ] +encryption_algorithms = [ "aes256-ctr", "aes256-gcm" ] +mac_algorithms = [ "hmac-sha256" ] +compression_algorithms = [ "zlib" ] +predict = "curve25519-sha256" + +[servers] + +[servers.sea] +host = "sea.uhsure.com" +encryption_algorithms = ["aes256-gcm"] +compression_algorithms = [] + +[servers.origin] +host = "104.238.156.29" +encryption_algorithms = ["aes256-ctr"] +compression_algorithms = [] + + +[servers.origin6] +host = "2001:19f0:8001:1e9b:5400:04ff:fe7e:055d" +encryption_algorithms = ["aes256-ctr"] +compression_algorithms = [] diff --git a/tests/broken_keys/bad_info b/tests/broken_keys/bad_info new file mode 100644 index 0000000..7a80c62 --- /dev/null +++ b/tests/broken_keys/bad_info @@ -0,0 +1,53 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/broken_keys/mismatched b/tests/broken_keys/mismatched new file mode 100644 index 0000000..bade642 --- /dev/null +++ b/tests/broken_keys/mismatched @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNl +Y2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTXRwCTNcWIwWe1HV/x +m4Id6Dch54nlfBRq1/5DROdn2AVm4ZYSj8kjHI8lDqfxzXZ66reHJAcecmNrgd4g +geeCAAAAsMnOb8HJzm/BAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy +NTYAAABBBE/n2mCAHZDC0V1sX473/Yq5hksUOM8woNApzcq4fA9PfEvbJtxR3ri9 +3RQaw1U+yQImCKvhv4uEDn0tf2BuOHIAAAAhAIJyO57UJPPaZ4EWPefrZ9/zVe6j +oI4ioVoVubbq38D9AAAAEGFkYW13aWNrQGVyZ2F0ZXMBAgMEBQYH +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/ecdsa1 b/tests/ssh_keys/ecdsa1 new file mode 100644 index 0000000..442c160 --- /dev/null +++ b/tests/ssh_keys/ecdsa1 @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTXRwCTNcWIwWe1HV/xm4Id6Dch54nl +fBRq1/5DROdn2AVm4ZYSj8kjHI8lDqfxzXZ66reHJAcecmNrgd4ggeeCAAAAsMnOb8HJzm +/BAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdHAJM1xYjBZ7Ud +X/Gbgh3oNyHnieV8FGrX/kNE52fYBWbhlhKPySMcjyUOp/HNdnrqt4ckBx5yY2uB3iCB54 +IAAAAhANE9lmc43afpjYdta5hvNo6Hgms6QC2ZiPMmdvfhP/8mAAAAEGFkYW13aWNrQGVy +Z2F0ZXMBAgMEBQYH +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/ecdsa1.pub b/tests/ssh_keys/ecdsa1.pub new file mode 100644 index 0000000..3187c0c --- /dev/null +++ b/tests/ssh_keys/ecdsa1.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdHAJM1xYjBZ7UdX/Gbgh3oNyHnieV8FGrX/kNE52fYBWbhlhKPySMcjyUOp/HNdnrqt4ckBx5yY2uB3iCB54I= adamwick@ergates diff --git a/tests/ssh_keys/ecdsa2 b/tests/ssh_keys/ecdsa2 new file mode 100644 index 0000000..50971a1 --- /dev/null +++ b/tests/ssh_keys/ecdsa2 @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRP59pggB2QwtFdbF+O9/2KuYZLFDjP +MKDQKc3KuHwPT3xL2ybcUd64vd0UGsNVPskCJgir4b+LhA59LX9gbjhyAAAAsAptbc0KbW +3NAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBE/n2mCAHZDC0V1s +X473/Yq5hksUOM8woNApzcq4fA9PfEvbJtxR3ri93RQaw1U+yQImCKvhv4uEDn0tf2BuOH +IAAAAhAIJyO57UJPPaZ4EWPefrZ9/zVe6joI4ioVoVubbq38D9AAAAEGFkYW13aWNrQGVy +Z2F0ZXMBAgMEBQYH +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/ecdsa2.pub b/tests/ssh_keys/ecdsa2.pub new file mode 100644 index 0000000..82bb939 --- /dev/null +++ b/tests/ssh_keys/ecdsa2.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBE/n2mCAHZDC0V1sX473/Yq5hksUOM8woNApzcq4fA9PfEvbJtxR3ri93RQaw1U+yQImCKvhv4uEDn0tf2BuOHI= adamwick@ergates diff --git a/tests/ssh_keys/ecdsa384a b/tests/ssh_keys/ecdsa384a new file mode 100644 index 0000000..92831b3 --- /dev/null +++ b/tests/ssh_keys/ecdsa384a @@ -0,0 +1,10 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS +1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQQNo4tCmBW6DJv2Xshowx5Esm8SL8iX +gSNgoMOv8T9f1upJnNZ0NNBq0DTk2EIAlGFjFRGwY1LMvuXCEjPcNgNgbLkRpuSBZEpUdC +v7vP+Q4CwAwa6ZLg/bUOxM6L3xD9wAAADY90b9x/dG/ccAAAATZWNkc2Etc2hhMi1uaXN0 +cDM4NAAAAAhuaXN0cDM4NAAAAGEEDaOLQpgVugyb9l7IaMMeRLJvEi/Il4EjYKDDr/E/X9 +bqSZzWdDTQatA05NhCAJRhYxURsGNSzL7lwhIz3DYDYGy5EabkgWRKVHQr+7z/kOAsAMGu +mS4P21DsTOi98Q/cAAAAMFXAE7Bhbcr4CW9xJhaXm4ToN6lVq3OKw4C41ZxxIetLxfYhyZ +IMpkNIlxhsT5JCugAAABBhZGFtd2lja0BlcmdhdGVz +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/ecdsa384a.pub b/tests/ssh_keys/ecdsa384a.pub new file mode 100644 index 0000000..3627b8c --- /dev/null +++ b/tests/ssh_keys/ecdsa384a.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBA2ji0KYFboMm/ZeyGjDHkSybxIvyJeBI2Cgw6/xP1/W6kmc1nQ00GrQNOTYQgCUYWMVEbBjUsy+5cISM9w2A2BsuRGm5IFkSlR0K/u8/5DgLADBrpkuD9tQ7EzovfEP3A== adamwick@ergates diff --git a/tests/ssh_keys/ecdsa384b b/tests/ssh_keys/ecdsa384b new file mode 100644 index 0000000..37d057b --- /dev/null +++ b/tests/ssh_keys/ecdsa384b @@ -0,0 +1,10 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS +1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSFuBPbGxyIn0K7nT7gLt1mO+RP7TuS +zFVatSmKS7UDl8EaBvEHSCqZWWvpaz1hw2l7fENakdh2aAmG4ZQiQU2k9LLzOJUmQvkJzi +x0d35GiyHvrgCbk+3jux0nRFqjf1EAAADg33k9/t95Pf4AAAATZWNkc2Etc2hhMi1uaXN0 +cDM4NAAAAAhuaXN0cDM4NAAAAGEEhbgT2xsciJ9Cu50+4C7dZjvkT+07ksxVWrUpiku1A5 +fBGgbxB0gqmVlr6Ws9YcNpe3xDWpHYdmgJhuGUIkFNpPSy8ziVJkL5Cc4sdHd+Rosh764A +m5Pt47sdJ0Rao39RAAAAMQC7FlZiz5mGgDtp1i9/Wy/gpWySe5xiER/COiIPFMm3wUchO2 +tbJ06GM6maApazHuMAAAAQYWRhbXdpY2tAZXJnYXRlcwECAwQFBgc= +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/ecdsa384b.pub b/tests/ssh_keys/ecdsa384b.pub new file mode 100644 index 0000000..0c62ca7 --- /dev/null +++ b/tests/ssh_keys/ecdsa384b.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBIW4E9sbHIifQrudPuAu3WY75E/tO5LMVVq1KYpLtQOXwRoG8QdIKplZa+lrPWHDaXt8Q1qR2HZoCYbhlCJBTaT0svM4lSZC+QnOLHR3fkaLIe+uAJuT7eO7HSdEWqN/UQ== adamwick@ergates diff --git a/tests/ssh_keys/ecdsa521a b/tests/ssh_keys/ecdsa521a new file mode 100644 index 0000000..a97e8f6 --- /dev/null +++ b/tests/ssh_keys/ecdsa521a @@ -0,0 +1,12 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS +1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBaJo3rKMWJqSqcuYkQExpvxGezbZj +khCmt26YtKCBxTyI+ptgV0wPujZOxuA+pYY909WGlulKHAzicP8feQZdUpoBK4eOwunitw +EAphr3I0dqcBAJWd4KIMov11qYvcNb8nsUbmulk10+IVxJQL+bwHhLsenHArFRlBrPiHv+ +6thVibMAAAEQwd7yr8He8q8AAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ +AAAIUEAWiaN6yjFiakqnLmJEBMab8Rns22Y5IQprdumLSggcU8iPqbYFdMD7o2TsbgPqWG +PdPVhpbpShwM4nD/H3kGXVKaASuHjsLp4rcBAKYa9yNHanAQCVneCiDKL9damL3DW/J7FG +5rpZNdPiFcSUC/m8B4S7HpxwKxUZQaz4h7/urYVYmzAAAAQgEt3nVbegR+JPwhB+zxq7Ah +1iKnuXdyNJytMm9PzOtC/0ufeCFtvWHB4J20ysAisdHfreftxJBh53yrSqmFYNmtkgAAAB +BhZGFtd2lja0BlcmdhdGVzAQI= +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/ecdsa521a.pub b/tests/ssh_keys/ecdsa521a.pub new file mode 100644 index 0000000..417ca65 --- /dev/null +++ b/tests/ssh_keys/ecdsa521a.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFomjesoxYmpKpy5iRATGm/EZ7NtmOSEKa3bpi0oIHFPIj6m2BXTA+6Nk7G4D6lhj3T1YaW6UocDOJw/x95Bl1SmgErh47C6eK3AQCmGvcjR2pwEAlZ3gogyi/XWpi9w1vyexRua6WTXT4hXElAv5vAeEux6ccCsVGUGs+Ie/7q2FWJsw== adamwick@ergates diff --git a/tests/ssh_keys/ecdsa521b b/tests/ssh_keys/ecdsa521b new file mode 100644 index 0000000..480a6ac --- /dev/null +++ b/tests/ssh_keys/ecdsa521b @@ -0,0 +1,12 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS +1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBYa7n+gYpTaP+AqX+rhHoYLbzSFjB +namxsbIoqAvhcqFXpXqDTyfP5bbe+/xr6iiJZOuAxrxt30SD6r5JR4ls48gAKHuqEOsphS +vKo4BucAQoJLfdD8Xl22RCRN9lpTzhvbl9iYUn0YV1scAU8xMmovBTdn9Z6QrIYqG5CMNn +2fIr4TkAAAEQo4W2lqOFtpYAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ +AAAIUEAWGu5/oGKU2j/gKl/q4R6GC280hYwZ2psbGyKKgL4XKhV6V6g08nz+W23vv8a+oo +iWTrgMa8bd9Eg+q+SUeJbOPIACh7qhDrKYUryqOAbnAEKCS33Q/F5dtkQkTfZaU84b25fY +mFJ9GFdbHAFPMTJqLwU3Z/WekKyGKhuQjDZ9nyK+E5AAAAQgCPHZ4xrsWwGgVy8H54DRD0 +g8ihcJ7Fsa7I84mTd0N4x2EbHvrmDChyreZ9MelKG8Jvwea0D4BtnCDPXEfkHAw4NwAAAB +BhZGFtd2lja0BlcmdhdGVzAQI= +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/ecdsa521b.pub b/tests/ssh_keys/ecdsa521b.pub new file mode 100644 index 0000000..5df7108 --- /dev/null +++ b/tests/ssh_keys/ecdsa521b.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFhruf6BilNo/4Cpf6uEehgtvNIWMGdqbGxsiioC+FyoVeleoNPJ8/ltt77/GvqKIlk64DGvG3fRIPqvklHiWzjyAAoe6oQ6ymFK8qjgG5wBCgkt90PxeXbZEJE32WlPOG9uX2JhSfRhXWxwBTzEyai8FN2f1npCshiobkIw2fZ8ivhOQ== adamwick@ergates diff --git a/tests/ssh_keys/ed25519_pw_hush1 b/tests/ssh_keys/ed25519_pw_hush1 new file mode 100644 index 0000000..865686f --- /dev/null +++ b/tests/ssh_keys/ed25519_pw_hush1 @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBUz8HBJf +hkFUWB/uRKkshkAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIN8pW2btKRHc65DS +yKjL7XBtvvPTHGd5KoI6L4KatxRZAAAAoDCMQNOT746RIs2IR7DixViQ4QjzodwVo//Y2Q +J/aE/PdKJ34kmngtnyyredzNseTG838n5PpW2Jo5R0JpGcRcyX/KC0lUHfDlnyHo29sNqx +fI39BEZnV87bA+pJhHKhLGxplWY9Hns+cpsh3mgNZGOC3M9zT1geO3AHdXMhI4HnYxw2/G +OUW1muIrFzKaRV2bl7kKwH1DRyYqp3VxfS+oo= +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/ed25519_pw_hush1.pub b/tests/ssh_keys/ed25519_pw_hush1.pub new file mode 100644 index 0000000..9d61e20 --- /dev/null +++ b/tests/ssh_keys/ed25519_pw_hush1.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN8pW2btKRHc65DSyKjL7XBtvvPTHGd5KoI6L4KatxRZ adamwick@ergates diff --git a/tests/ssh_keys/ed25519_pw_hush2 b/tests/ssh_keys/ed25519_pw_hush2 new file mode 100644 index 0000000..3b29077 --- /dev/null +++ b/tests/ssh_keys/ed25519_pw_hush2 @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCuOPKhvD +nx/dHJW4bC7y4uAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIET2SpCeHNG/3IIL +j80xTImmn2JEcTRVM4khSQ/rCQ3FAAAAoIoSFrEKoEIGSAsr07HI/tSt2W/DKgeTYsUmNi +r8suuyfNqETjlfZ9FWxw0UWW1mxA/XzLvRYx+kGPkqKM8DsCpVCMl2S/3hMCCoxhE5mKP9 +PE+VV0DKa6jywEcgMEiMejoRNnfHAAvI9KHAPPdIR11geqEMAx3nZFRy0IyZwxNYPFbijM +mjv8RcoFGVoHT2R4abpO6oEADjJ0KqZRNdg9k= +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/ed25519_pw_hush2.pub b/tests/ssh_keys/ed25519_pw_hush2.pub new file mode 100644 index 0000000..601efde --- /dev/null +++ b/tests/ssh_keys/ed25519_pw_hush2.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIET2SpCeHNG/3IILj80xTImmn2JEcTRVM4khSQ/rCQ3F adamwick@ergates diff --git a/tests/ssh_keys/ed25519a b/tests/ssh_keys/ed25519a new file mode 100644 index 0000000..617c6fe --- /dev/null +++ b/tests/ssh_keys/ed25519a @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACCA4kdqb8sTeg7amwY8Tdck2zEbqcXDRFva/4VRFWNY0wAAAJj4Nb/Z+DW/ +2QAAAAtzc2gtZWQyNTUxOQAAACCA4kdqb8sTeg7amwY8Tdck2zEbqcXDRFva/4VRFWNY0w +AAAEB+W/KcnOrffyr18T1GttW8Z6yutReqViIkm6cgOUAA7YDiR2pvyxN6DtqbBjxN1yTb +MRupxcNEW9r/hVEVY1jTAAAAEGFkYW13aWNrQGVyZ2F0ZXMBAgMEBQ== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/ed25519a.pub b/tests/ssh_keys/ed25519a.pub new file mode 100644 index 0000000..62c0e32 --- /dev/null +++ b/tests/ssh_keys/ed25519a.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIDiR2pvyxN6DtqbBjxN1yTbMRupxcNEW9r/hVEVY1jT adamwick@ergates diff --git a/tests/ssh_keys/ed25519b b/tests/ssh_keys/ed25519b new file mode 100644 index 0000000..4d2a5ab --- /dev/null +++ b/tests/ssh_keys/ed25519b @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACCfk3xXxqLhgBYvgUyAC+vBu+3XBD/CY8SY7wp2hrXhEQAAAJj50iU4+dIl +OAAAAAtzc2gtZWQyNTUxOQAAACCfk3xXxqLhgBYvgUyAC+vBu+3XBD/CY8SY7wp2hrXhEQ +AAAED8AA2vVnMt9sKP6psihSU9ldHjV9yv1pOA2XmdyDcCsZ+TfFfGouGAFi+BTIAL68G7 +7dcEP8JjxJjvCnaGteERAAAAEGFkYW13aWNrQGVyZ2F0ZXMBAgMEBQ== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/ed25519b.pub b/tests/ssh_keys/ed25519b.pub new file mode 100644 index 0000000..4b805b8 --- /dev/null +++ b/tests/ssh_keys/ed25519b.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ+TfFfGouGAFi+BTIAL68G77dcEP8JjxJjvCnaGteER adamwick@ergates diff --git a/tests/ssh_keys/rsa15360a b/tests/ssh_keys/rsa15360a new file mode 100644 index 0000000..a1c8ada --- /dev/null +++ b/tests/ssh_keys/rsa15360a @@ -0,0 +1,170 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +13aWNrQGVyZ2F0ZXMBAg== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/rsa15360a.pub b/tests/ssh_keys/rsa15360a.pub new file mode 100644 index 0000000..a7a2e24 --- /dev/null +++ b/tests/ssh_keys/rsa15360a.pub @@ -0,0 +1 @@ +ssh-rsa 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 adamwick@ergates diff --git a/tests/ssh_keys/rsa15360b b/tests/ssh_keys/rsa15360b new file mode 100644 index 0000000..0ffe92f --- /dev/null +++ b/tests/ssh_keys/rsa15360b @@ -0,0 +1,170 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +dpY2tAZXJnYXRlcwECAw== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/rsa15360b.pub b/tests/ssh_keys/rsa15360b.pub new file mode 100644 index 0000000..d0672ab --- /dev/null +++ b/tests/ssh_keys/rsa15360b.pub @@ -0,0 +1 @@ +ssh-rsa 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 adamwick@ergates diff --git a/tests/ssh_keys/rsa4096a b/tests/ssh_keys/rsa4096a new file mode 100644 index 0000000..f05e13d --- /dev/null +++ b/tests/ssh_keys/rsa4096a @@ -0,0 +1,49 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/rsa4096a.pub b/tests/ssh_keys/rsa4096a.pub new file mode 100644 index 0000000..fa7f567 --- /dev/null +++ b/tests/ssh_keys/rsa4096a.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC3ftJT8JKsBlMHj+mJ2JLUCH7da6Rn2KxKO4+9LzoZRnylf8EB7uO/nq/tyLyMMHBv8d25m0xne498z4VvKF/r4wt/gvWkmcauHL3WqTTJBfzc4oSGacVrCvUXX1LaSt/ZSuIUDLqWBE4lONFmdfInaB8ozqWjIgX3njhw9yNl/k53ZnAWiaOnG71tlIWha+jxubZ/T7RTp/4tiWpubWOF4QCDAbAAijDe3C8wvIlmKihZMdl0nPLx11Ope+uX/VMTZlmdYUpy9KkiyKwO2A5PFVmbqpb51WHVBEwJDVpOOda+Fow24R1ZWqVcUGtvau1jBLxC7MvqNPx1zNHKRWbQyRSug9B8DgYdTxVkU1bb8kmDA67apyl8Qr+CB7xECRUyTcDfigSJ2djbBaVgIe3LVHQeJAZNaZNy6FnhkxpuSBYxOBAOCzTrIIacYGivMF5/JjfO2cFH3y26UJbP+PXoZSYYSojg2Ksk3j+pZJTjr3tDqnJkfO/bMId9cNe+Csp55rg+IzcXfQxBPdmS1oyVi2MLY0mYDx/BlZRvIg5Hj+4SuY4owpSi1Ap5aZOKb/Su0YURu2zVQQBxmyTkbQoFB0wopoiM6nQZZCZayCjM36jqp9rsA83L89drtkrYUESR3rJ2boUhSy9lV3bT2frSmMtHqjNpToN1/o6sCva2tw== adamwick@ergates diff --git a/tests/ssh_keys/rsa4096b b/tests/ssh_keys/rsa4096b new file mode 100644 index 0000000..0dd047d --- /dev/null +++ b/tests/ssh_keys/rsa4096b @@ -0,0 +1,49 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/rsa4096b.pub b/tests/ssh_keys/rsa4096b.pub new file mode 100644 index 0000000..4809ae4 --- /dev/null +++ b/tests/ssh_keys/rsa4096b.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCzT0NGauteX9iS/LQu3gunlDM4G8sCB5mG0diVelmCoj6QpeGXSc6LIH9AQSd5XFQ6p/nzsU5NeXjpjsOkov7ADl6MLspx0QcvT49KwpAk5lHU7JVAEq9BwRC74MHbKXng3Rc18bEy1RVJHCurxLEbOYlNbGOTGsFac9M90SPZAg/s+Rg63HJvrg6HTI0IjyjbVDuCErELGdpj2xc2prWzudGmuMkSqlX3hbv6KOrUuUFobwRnyRqF4sWSQ2Mf5stji6DIzaSLWBB5GaLMYDe4xWBC4P26/fzRF3zzf7t9WlR4G+2YFR9uzHwZmdRpsVKbWV4epFNVOvJ5uFEQyew++Advs4D41qFDjOUHkhH7XvWR+cdqMxkxw3Vg+aF1+lYFYiMyeaUxXeYokS7wmHL01F/gGWHrlTNwzC8qRBBnisNihuDYn0aZoxL4gaJKY+9bZhilvbk2+Xp5AgkE36L+kjPFZYKsEChGy6qSK/Mgoa+HQgiCDQCivXVVCGnAX4hWW5wzkgdCMiOV3cR2DXIlAXgKzsrD4OY+Z2waKEw1ZsMiuA23vAdjPddKmP0TXbDjLTEAgO7iXeV+RIq22T34mS+MGW7HsXH4ifzOxFTi4M5eZyY853HDFf0SUijVNzuZfQkDDjrRNUnwGhamiKgfu3jRO9mWrLUewtJbw/RW7Q== adamwick@ergates diff --git a/tests/ssh_keys/rsa7680a b/tests/ssh_keys/rsa7680a new file mode 100644 index 0000000..45de061 --- /dev/null +++ b/tests/ssh_keys/rsa7680a @@ -0,0 +1,88 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAD1wAAAAdzc2gtcn +NhAAAAAwEAAQAAA8EA2sM4ZrUvkz8+LMXSKQ/lITA12pu3U4N820TvFgs1WEiJcuU+7SX1 +dxAbc30as47FfyocmOnNrQNLBfMUm3F/2s+oFnGEjuAQ3iihY16u1QioiunkmU9ChNlWN7 +HVXT/7kUb7l7GZUTc/fFqb+p0dqF/vDfq3/l/DAKLjT3odjAHD7AD7GbOmmjpFUkVqWQhm +geziQ2Oq9lp0uvCvCpCQN1uReQT6+Mh0qc26lmsb3mMq8d3rDRaA61GXRYRVZrLizuAfku +5f3Al1XxKDSi0W1DMkpsnV2jEsxPOSx6sxpbyAi4X+EcpTMtVpmFrmmUqCI4LtwHsuCgJ1 +Pb7Rb+xjAfiJr/Z00Q1DeVOcJtCZvMNI/G/WqVKNZUNuNQkvQ34yygYAcMDb+/wvpt3vau +Xq8HJlTTYrrWcBWtOxRGQX8y/R0HZy5PACTaZ9Uq7e+fo87FDhbQ0hDWgW46M2aKJCHtcj +puXqzOOGNBmhGDJg7ob6r7wecTsigagLBYTfSpjj9qtarbTGxgXtdgriIJQfEao3o2v0/5 +9NlVEp4ii2dV2nJU+YNncDWbUmZbjXYqeSd42v5GArfqVHp2Eog/hRJY4KHwBItL8550nI +4z12tkPfDeI8Pat1u/m7ZjgBHaMicc/LMohPBTbxR4DgjAZu+aGooc4M2ZM4KXAzHQYyIw +h0+DzN1Jnp4LtXYN2BaDi3FO4/qMbkmo1Wy4m6HT34CXEJeV2FVlcDmDOf2HXcsMa0uoY0 +tLXPiRs+KEmRjfEj0sNYp5ICeZkEsjqbmEniZ2Q4S4BrfAd/HzrO7PYSOODEnn8H8nK1Vr +dyk8MvlCXYRokSqp6c3ab0jV3ZEKcBQ21MGSYuoSb/Mz49mjWXp5tSI0PBCTfzUtKdsWFC +LgYvfWnnDvV39rCGAffCuimQjGwdV6IFBNIHSngIZFNfWSeP2WlrC4lYfe7tpoXKcgwmQm +NB15Kb1Xi5CdYdsIKJneqVl9X4cwQRpJ0wlk/AeUGdHfL8KNY6q8zGmYKWm1NPGkoDvzlR +3dGMrc+OQF/EGK4PRKRT4AcZZxdfhxNVq0+9J25MsdOjhTcOlaqVu7SwWFatRfWsuyUzm2 +pHcHwycLvNg8t+O1ywVVVOFSgtqXOdHyXZhDuV+HUtp9xU/fbCamA9bW0YwF6K0CIyWHCz +h5hyCGpGUMRLTn+UqUmkrRQ/6R+ARm/8KyoJrcyjA+GGuwZIiEuX+qaKbwGj5ipQmCiR6x +UK2BcTAuqe8+Vl30yD6/LAk7Cy1gMtSd92ZNa/AAANaIqg2vSKoNr0AAAAB3NzaC1yc2EA +AAPBANrDOGa1L5M/PizF0ikP5SEwNdqbt1ODfNtE7xYLNVhIiXLlPu0l9XcQG3N9GrOOxX +8qHJjpza0DSwXzFJtxf9rPqBZxhI7gEN4ooWNertUIqIrp5JlPQoTZVjex1V0/+5FG+5ex +mVE3P3xam/qdHahf7w36t/5fwwCi4096HYwBw+wA+xmzppo6RVJFalkIZoHs4kNjqvZadL +rwrwqQkDdbkXkE+vjIdKnNupZrG95jKvHd6w0WgOtRl0WEVWay4s7gH5LuX9wJdV8Sg0ot +FtQzJKbJ1doxLMTzkserMaW8gIuF/hHKUzLVaZha5plKgiOC7cB7LgoCdT2+0W/sYwH4ia +/2dNENQ3lTnCbQmbzDSPxv1qlSjWVDbjUJL0N+MsoGAHDA2/v8L6bd72rl6vByZU02K61n +AVrTsURkF/Mv0dB2cuTwAk2mfVKu3vn6POxQ4W0NIQ1oFuOjNmiiQh7XI6bl6szjhjQZoR +gyYO6G+q+8HnE7IoGoCwWE30qY4/arWq20xsYF7XYK4iCUHxGqN6Nr9P+fTZVRKeIotnVd +pyVPmDZ3A1m1JmW412KnkneNr+RgK36lR6dhKIP4USWOCh8ASLS/OedJyOM9drZD3w3iPD +2rdbv5u2Y4AR2jInHPyzKITwU28UeA4IwGbvmhqKHODNmTOClwMx0GMiMIdPg8zdSZ6eC7 +V2DdgWg4txTuP6jG5JqNVsuJuh09+AlxCXldhVZXA5gzn9h13LDGtLqGNLS1z4kbPihJkY +3xI9LDWKeSAnmZBLI6m5hJ4mdkOEuAa3wHfx86zuz2EjjgxJ5/B/JytVa3cpPDL5Ql2EaJ +EqqenN2m9I1d2RCnAUNtTBkmLqEm/zM+PZo1l6ebUiNDwQk381LSnbFhQi4GL31p5w71d/ +awhgH3wropkIxsHVeiBQTSB0p4CGRTX1knj9lpawuJWH3u7aaFynIMJkJjQdeSm9V4uQnW +HbCCiZ3qlZfV+HMEEaSdMJZPwHlBnR3y/CjWOqvMxpmClptTTxpKA785Ud3RjK3PjkBfxB +iuD0SkU+AHGWcXX4cTVatPvSduTLHTo4U3DpWqlbu0sFhWrUX1rLslM5tqR3B8MnC7zYPL +fjtcsFVVThUoLalznR8l2YQ7lfh1LafcVP32wmpgPW1tGMBeitAiMlhws4eYcghqRlDES0 +5/lKlJpK0UP+kfgEZv/CsqCa3MowPhhrsGSIhLl/qmim8Bo+YqUJgokesVCtgXEwLqnvPl +Zd9Mg+vywJOwstYDLUnfdmTWvwAAAAMBAAEAAAPBALYgxbosqnkqs/bOk1OAWkCxRITGE3 +DCDZb34x01I6pmaZhwZ11EtwHzNQeHZk2LVb2zL6/XJ1cdYL6JS+TGL63aKJTW2Yeh4Ck1 +Jnf2ghP2a2uLorhIlpbH4tHnij1iYWzn7dqzD3PgTUiYnzecyu49QGchD0IGM/E5q4mlny +fK6HR5tJQHT3MjhEckZ4/MQJt2vkFgnxsO4BQrAXAIPyj3YTuh+9hX+1jLYMaOUdtqMHzB +R0nULGy9tvU3YWppEA8v5NmM/93POhp27Ts6IsFz+tWpQBOx0RX/u3nkeycCsvp2CbqB+Z +ZeutUPCOEieQpbnNkdNI080qMfVHqcESm449jNlR/erQg7pcti7DuNUhxoeAzsH6/o3b3l +8aV9UYeES6WTyxIVOQ7xwrv6wwiAFPqdWOu60BPwHqtTseTTMRkfJDSZ5TEEpV3LHPR9c2 +9DPwptXdEtkbDfVxLx056dep8e18bQvhBuLgJZHv42/kqEkcuvceEEKHjl0IjolRHuQ0ZP +NRX0JWibUvvQlbU9Q6kY3hZbaFoiAn65an54BAo6I/1kRDPRbzBNHXSTEovaOFAoCM4diH +Q/nV2RxO1BPgflUqK4edqnQUp/B3BjPTbv3Ttynkhrd6t4gOVNxHtqCsFcPGWWgXu9bBjC +iNfI2bDXSF99dc+2ISl1QP1BLFpgPFyWlXDZsYD+60g4/ToSuYhN4GN/ew+uInbXNi34j2 +m6UzhCyb/cuD4mktiMSMhVeQ+Q4pvxRRYyke6ozmrvlOgfWhdhB9l7xj3SzWrWQd1v0/x2 +omMYQYcpoGKJlGGnUHY0j+YZtzU1GdyJCoV/Ou8BAYpwbjZ4lKJ3SeugJwG83oku/JIO/G +F84eKgnOkbQUlMUqysMRBBO20qv6z6EiYp0y2ZsSHDHKDCD0A7iQa5Pn/v5ZcbO7gAq6fj +aK6+qb4yKE4QztXl8L+HjXTNVXYjnxOozL2A0eZTt+kdWnpbDOFN9G0L+S86+QfAQpkJuQ +NlobZlW374KASSlPqkNPcEO8fj8tEbpYjoaeveZzPkIPsOABIIjNiSbdzuRtj8sa0VWWme +tM8UymW3REnaxHRYPo+jhqFLDbnxnInwTs2F2f6nvc9XZx9nlQaCZUWMf+maaM/5+Qbug0 +ZvY6ERPOPfxmkWb35ZQydA9mRZFGDzRl0dSHV3nrDrDpVNYhjFVAza+eRX085b6M9eo12y +TxkFFLbA300BGrZjrAR+KK5r8njYx4W5P6j/3u5dF7mrrs+6SnmtTo0CapFXQQAAAeEAso +CVE3liuI/fqKy2p0qreKN2fdsKw6yLy1FEA4adWXG3gpr3dfB7fRMJ2dq2Ugqx2ljsvQUv +DOQC8VG5UYADJiV8dIm5LGcceNtbuBxgSOMCosExhrV4OfS4GTdA+yqELigdgfJPXGnmkO +obFRbjU3FdHnGiM1ldCTxB0zhND6OO5Q17NOfFt04dHz7XJPv72pxXxXeiYQb6l65U/iyy +AA/6pX3uaAZ7R+oingDm/BDvHYUpMEEJF7avbUYS8I8aJ+7hL35Nhu3CVxtgO/Odw5fI5d +IH8TklQ1X/2/n0ChytOrywlOn7DSazxSA6xoq5lkmkOgBd1/Ys+rS8fP7vbyLpdAlWdQQy +qjFf/38tfija5xFEnYbx9D0LGPkzpdBejduygHS7+lAMZmYOZQtZ58HbvagvyrylTftvGA +gyipUDIttp5KwQgaPvPNK/i8ESLlGx7ZGK6k9B5lYLUGyg92nxfV+t6xPK3Uo8/1z8B65h +0K1347+4jlh9kd2ooiyFDE+BbJjtgj4sR+USgAEXVMW1oQGm76nHtI/tRgBjlGg65AUym1 +2Jl4+0xSbGwnLwIy6GTcPQCg5PG7iAAhoG5Hfgqw/UJh5qnFZIxh6qLbKQbF77iMRmGWXq +nLhZ548aAAAB4QDwID/SvOf/WkdcG8y7ONFySVxwnKPTRCG9TB9/JvhvbnjLMasfPu2SZw +SISfNvjt/9vUir3veOX16wCa7dB03NtBo8Lgyz0eEADv+nCHRYO9H8MLmzPJOQnjJm58+w +Z+Z8ehOR2MlHtrCCDdrFHqdoAsmVu7MinJTRzBoSx3t4NwDqq79yjOxYhjfhLPBpr3+GE5 +QNSwk469TIzJYnNP8wUP9Fw6B9H/aKHn1VnEifc9qGL0KxlqCOAW27+7JPlqh0XX0Jpo8t +/A0RnJj6/GrURS+3B1FYxGQQ2rRQ1sv59sEHvqfjLczLQHsEZYTNqbBNrqBPBl/IQ9EGOR +f4Ip6SJ0I623MztsbinL5pdm8abfm2YM/mvOsL9uluCHvT/v3O9zpdHXFYapHqDoWY/N7X +qIvumfoM5R0ZguYpOH478Z0+W713/4FWstfwSTzLmYxGUFpJLWe0bwHgsUFam6GexKpAv8 +607v3ggDWyHdyg9EYTnoT2C6nYo1YafGdxDKhTSVAY/J6Qv/CJr5Xzi+Hr1bFNgjpESO5O +HWEVEWXSAVN7i0HHeBQm9GKT1c8hEFVloiUgY2jyuKWltL2jIcTy4BGj3GOKRm4ZOgLQ33 +kwlMHItXo4XjfT5uggxMg0728AAAHhAOk5bfMICrSo1108Lvi4EATpR7VI05o6FJsZC+od +EnJYfOL0mgBFeku5iSzShMniQiB42yj5o62s/GZXBGfLx0ocTiB44FxgP05FfMZCpA8qRe +ZAYQ37gk7R9vQ3M3gyXldWUtCVE3C45yVRyt4RZn5Hjtd3mOJL6Dh04zE68xHhWZM1Zl7c +m3WkCMCpQ0JImHmL7eVsMoOf/g6S6siVZIaAx3HSbYgeF03LG1+Neomca4KBGZq57+yjVH +DW6SA78zabrlfeAZteHns+sNPWF/YXSfQcEiIqYeixqy4puj8TAI/aHF2XrMP4ePbNUOe7 +GucroEASS6afkgAnHhDbRHES/J7a8Co0A2YHb5RhPcOj8rXlb26E010aH1MA1TaD87G7mx +PmBQE7gEEB0RIlckXQrVYxlwU83Wu1YQdzuH/kVUUyE50mzXqSXyNg8kRkoCW8My4xuINw +5yIws0JZqWE2/byejdgdq3ELh1P3cZNrEAdOlr+XbIfau6/8Ag8coiwWI5dclQ6mdERcZt +j9alokZcD6uV24IolqR7ufoSF9gf777A7pT5uNm0lMKJ2rnc8XaZczWW/unbHxMVW0k8Ci +3wO4dPv0PqLgPr0NJoEmXXIYKNjtyCJOrdEbi7PlsQAAABBhZGFtd2lja0BlcmdhdGVzAQ +== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/rsa7680a.pub b/tests/ssh_keys/rsa7680a.pub new file mode 100644 index 0000000..ca53d7b --- /dev/null +++ b/tests/ssh_keys/rsa7680a.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAADwQDawzhmtS+TPz4sxdIpD+UhMDXam7dTg3zbRO8WCzVYSIly5T7tJfV3EBtzfRqzjsV/KhyY6c2tA0sF8xSbcX/az6gWcYSO4BDeKKFjXq7VCKiK6eSZT0KE2VY3sdVdP/uRRvuXsZlRNz98Wpv6nR2oX+8N+rf+X8MAouNPeh2MAcPsAPsZs6aaOkVSRWpZCGaB7OJDY6r2WnS68K8KkJA3W5F5BPr4yHSpzbqWaxveYyrx3esNFoDrUZdFhFVmsuLO4B+S7l/cCXVfEoNKLRbUMySmydXaMSzE85LHqzGlvICLhf4RylMy1WmYWuaZSoIjgu3Aey4KAnU9vtFv7GMB+Imv9nTRDUN5U5wm0Jm8w0j8b9apUo1lQ241CS9DfjLKBgBwwNv7/C+m3e9q5erwcmVNNiutZwFa07FEZBfzL9HQdnLk8AJNpn1Srt75+jzsUOFtDSENaBbjozZookIe1yOm5erM44Y0GaEYMmDuhvqvvB5xOyKBqAsFhN9KmOP2q1qttMbGBe12CuIglB8Rqjeja/T/n02VUSniKLZ1XaclT5g2dwNZtSZluNdip5J3ja/kYCt+pUenYSiD+FEljgofAEi0vznnScjjPXa2Q98N4jw9q3W7+btmOAEdoyJxz8syiE8FNvFHgOCMBm75oaihzgzZkzgpcDMdBjIjCHT4PM3Umengu1dg3YFoOLcU7j+oxuSajVbLibodPfgJcQl5XYVWVwOYM5/YddywxrS6hjS0tc+JGz4oSZGN8SPSw1inkgJ5mQSyOpuYSeJnZDhLgGt8B38fOs7s9hI44MSefwfycrVWt3KTwy+UJdhGiRKqnpzdpvSNXdkQpwFDbUwZJi6hJv8zPj2aNZenm1IjQ8EJN/NS0p2xYUIuBi99aecO9Xf2sIYB98K6KZCMbB1XogUE0gdKeAhkU19ZJ4/ZaWsLiVh97u2mhcpyDCZCY0HXkpvVeLkJ1h2wgomd6pWX1fhzBBGknTCWT8B5QZ0d8vwo1jqrzMaZgpabU08aSgO/OVHd0Yytz45AX8QYrg9EpFPgBxlnF1+HE1WrT70nbkyx06OFNw6VqpW7tLBYVq1F9ay7JTObakdwfDJwu82Dy347XLBVVU4VKC2pc50fJdmEO5X4dS2n3FT99sJqYD1tbRjAXorQIjJYcLOHmHIIakZQxEtOf5SpSaStFD/pH4BGb/wrKgmtzKMD4Ya7BkiIS5f6popvAaPmKlCYKJHrFQrYFxMC6p7z5WXfTIPr8sCTsLLWAy1J33Zk1r8= adamwick@ergates diff --git a/tests/ssh_keys/rsa7680b b/tests/ssh_keys/rsa7680b new file mode 100644 index 0000000..3de6692 --- /dev/null +++ b/tests/ssh_keys/rsa7680b @@ -0,0 +1,88 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/rsa7680b.pub b/tests/ssh_keys/rsa7680b.pub new file mode 100644 index 0000000..628ea82 --- /dev/null +++ b/tests/ssh_keys/rsa7680b.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAADwQDrtJEKsOQsAUx9qmDa82CA0HswihkxCOpFJWUXFvWlxuqrCxNck7s3ceyhhxDlRpugq3DKPxQgFUnBnFhgNmqmeKFQdI4fjn5EhEptf7tu0+aF4rnUj4DnD4uzbd/b5J8BnMQ8iTzdXuxkILUeugZiyYjJz9dnJY7uQbAfjz3EHSLVcxryCd4lqnbLE8b6cXuZkUKva4RhRNUajyfoProkiWD6jYIebeMt/ZCMddatmGtH1+dVmxPs4Q5DiTDcS8w+1YKCcEYaKkMkzGg6QgIbs1Do24qJGq+E8hLX38G+5sJGZsCyy/tIxnzOIZuRDvrpJMZmiaBgtjtUriJmod3gJPFL9DRztetUZUI7AV6jDrvppiXyf+LAphUzB/Z0mvsM/pSk780DCqdzlYsesgFRlCPBy2ZXxQxrHEWzwlAmqZsMYJ7G9KPu2OqFclp7qIPgoftx4RnKSH/fEaaixtoYhPVQVZWWg9n5Yhxps7NLGKYpBmCD7iDRfvkSxKSVrIPuMmz16fAe3F0AAxTwnoCnQ4ILW/E/wQY3wp03OzonY/r+G6u4x1OpxcIgdPA2p/qbTWhNa/NRz2SxlB13DI9YLTTMKnx7GVzLUcaAnimQUChEQKuAdsobkjB2Xp6PVDSt9VtuQZ/xCebNqZPRsm+gqK9sJFmXXJqAXGc+e+q7XTtrukqMvi+B2NGIeP3H1FAqZkrfUwOjq4BsSrESPrRXqAUq2ElY21hsh4+vOio4B7tnNzc3BzvExvuZoFbIn4khfJMCNZqPu/FsCYzKOk0wV3yeNu5qBjNmEs+9CohuN9RUsV3jeGgebw8gdJu0dOcsrWst3DhyiqfFZyJNI9moKTterSTpb7vW6juP7cdMoaUPm4a9j+LNbpYEjOtEetofr32L3xiCVcVIDTz7MdhWER2On8JS88M/vUKS7F5LhiDhmpZGNhSVlnj5Fr+EoDQlckH9pm7ytqrKHTG5rg8cVfnVbaJjiFQbXzvGcepHv+LrySyHTP1JyrgZdigRd6dK5V82hXebxhNSbmoLIQ9U79gn0GGs7rGGuldusX4uisAUMfWB2JKdk7/uFwxoKD8wx6EumGUTpI2wsTblnEh2jYJjDiSSLq1DydHzXfESK/upXA8UawUPOfIDcTOQucdeyMLhIsg5HZMR2tn/SxaLEoza+HzvrevgEQhGU0VC84HVb7eAXYIDoLf8yOabhi+2I3UCinQGA8PcwEYhvJ7opOU4HzTg/WMMI9+e3s12cGeKy+HzYRSBEyqXrwSFGIc= adamwick@ergates diff --git a/tests/ssh_keys/rsa8192a b/tests/ssh_keys/rsa8192a new file mode 100644 index 0000000..397a531 --- /dev/null +++ b/tests/ssh_keys/rsa8192a @@ -0,0 +1,93 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +VtmGIwAAABBhZGFtd2lja0BlcmdhdGVzAQ== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/rsa8192a.pub b/tests/ssh_keys/rsa8192a.pub new file mode 100644 index 0000000..18519f5 --- /dev/null +++ b/tests/ssh_keys/rsa8192a.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAEAQDC+RrYSdcY7hMA8xtWk3KRdV2BhjqCMkbOpkGA6ri2mtkeGgQEgrUtK2pVSpkU5e0hsQhwiiapgXLDhtw7vYrKY600SXfDDqWFapBcEzD+/S82BlYY3g3LybD+T4m0zrPIhFsvkt0yVCRKEz8zO/tMc20TZsJp/72oRl0fNnTe9KCgNpn9iiMtH6vDRKI4rGZju9nVqCNEhRhugALpfzFFdLtq0JuKwBWBik3xZvkzMNauvHuj3uF/FsS9VQQi8sP9J81lB7WtyTertQrKpTe+qEJf91Te2zxl5PZ0yoV10l09M0khue2O3xrZi+o/IuCaeCgs9dDkQ5KclDxdiRZv2uijzdUrTPHQZBF5cD3EnVZ6VG+v0S49AG07NVd5KJe/OdYpocfcrirdrYJ9q/6W2loDRBYswCwHSl7SPGFBvA8e2TZ8CRFtso1veU+AT+pNUwvmiqv8YO/JAjTCfcEXZW2swcJMwiebOyEZGWixTlepxwNqsobhs7+osKvhgaTTj/BKPSW9LrViy2/V2umunyzgDd/Ih3a8Ll7U5k85Cl+cWqhPIgVvQqDxUgUfxLgHFeGu4Y8wPZfsDw4LosyBaGlLfNAzsrjhIiCeN1l4+6WAoYMtIuINmpm/4W3cwo5SiaO8C7CMiZA4cYTki4LFaYbiCo1oghqdPhMOIPCBol+fuGAeuEP+W+YD18BwzS+/GE77XFSKbF+3N4XfPxWRyBNOEe0Ul2XA8rytr+AwmkDVUV8AQ/WnOpleMVW1Uz/l/DzArEKRYEUh30l71wFJP14ngPppggpEnhysogtcb3mbPYDiUGGX9/a/YRU2qvWshJRLdr6b3AxhrCDw8AoTuorTTaor1fjcmdcKn9d2ZEnxkN4yc1368EKS728WoF/qj7GwJhzs3w7/CkJ7WyrVeG0vkRfcZVJl7LBgAoeRPTyZAY7AgL5nf4qpCTsEXuTk/pgGaUknhu8heVzL6gUtRXQvlJfxZgOdusuGMQ6ZPbfYeV4+Lwpw0lLAXcH0O/tMQoGnB097zDTLzLt3Swdfdo7e6Ef0HnzDGCGRF/hFmr6VAGbpu7nhGX6ySSNx3gpSj1NRMCqWzml/uPSnNK4Wun/tycscWaxB1GEgvl+G4bNzrdFW+GZenbVCvkCyQua2u0JQERGr1JZ25nGvCIDIl7i6EW1dvFUpJJ+OZ87twYGH5xZc3oOII/YSzlUci3E0LJUFodfSUoO96FoL/c+2uvz2jGHlWdqDN5klywX1bjjzD/HtrfcIcHBRa101C+ClUUWABsGELpfUu7lvjnb0IOER1NE9DJRNLatejLdKZ+zz2lT4gmo7GtigOiNW9OpefihGKkBu8ICckT3Gx9/P adamwick@ergates diff --git a/tests/ssh_keys/rsa8192b b/tests/ssh_keys/rsa8192b new file mode 100644 index 0000000..1dc80c0 --- /dev/null +++ b/tests/ssh_keys/rsa8192b @@ -0,0 +1,93 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +00UAAAAQYWRhbXdpY2tAZXJnYXRlcwECAw== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh_keys/rsa8192b.pub b/tests/ssh_keys/rsa8192b.pub new file mode 100644 index 0000000..c218deb --- /dev/null +++ b/tests/ssh_keys/rsa8192b.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAEAQC+hQbPKvQ/05nzMyC8/3t5HOAlDtdCvlYD9/+BEYT98pn/iQtlLzYj4yA7BwjTNXNG6l1Z3MZz6TTbLXUl5SJlNWwt00G4jHttCVmpX0NoKIAd4RMIGRFw2uXmjq0qz5Fvkupctaejyrsf6P4YNktuqp+Znhxpz1SgfsXAiO+gSev+i7YCwVl0vuV6KTsUrqBtj2l7cEgfbJSo1zvII40WojouX2u4BOTFp8oOG0asnVpbsicqC/9TNgXUypH/WsjCNpL8//iqAUJb5Ptcje8yVQaOU4dNerpqdNNxKZO5TGWNKzXAaRLel0oreShLp2U7FDFRpq0MfRLg+/+krvqFmlDgnrK8NUikuVsJarHDTBTaYhxbpSyRiZkcUptzhdnI1kvJTzq4LZs2LOQhvomJ0qw+XJL4tuucjGtgckIlqRxEnAukCydSHYsKtDOurW/AK37kBf6hhCxiFrGncNX410M4ZYD9pNe7ix03XqGHfEuVVt1rB/493yw08phDVdEXIZvWxs8/xLYuYJW81Ph+bsVx/2Od7vbuA9x4BT2+Yirsu20mH9BtBudKxpxrIncHujuGfVsBtESkw4x9am4X7DSUwVQEoSwYHWsPVNGyJjayq+hh1mLJZINMxnxqN2Sz/YhT34creDYFVKkdXDM63wxQe480hdaENuxlLGjErJmqGoImbX6GjyI2jCwd5AWVsOV5mF1QEfCI1yZCyUNCFFd5CGZHswtPhwyPk+xcgAWhHlIB9FAG+qlx/ISA3jzO9W9+yZGghcD9ZexOs5yGkbEoEdxanl3aPNs5JfmehNLWE3Hq9yg4s7iPpH37pnUr0M1XrlExDvG1EO/T6OObwTujFUmlzNSUP/jFpJQnjGvFkeZXaw3pM5r/lLee/0oWUeS6JOEB1qMtP1UZJp1bwJvscoJmfTVLTK3ChR8/rOQqZzRzF5h0uma5HHVeKzzlCUt3qApdwkMtHjvAm4EbKBD8Jis1qQXF9pyfcdZeniPaIPnkeENXDy2Eo/mv83VPZcfm5S7cpLikmzLApfDy52ZEVPsJqmxAx5j80nlONnfwdmtjFxBeisB5uW8fdCLMKbQHT2O0U+aRpbVP2ScEaV6M6G+VahHIrqsmAFhc8kmpvCgH5R9ZoPEAWvSRwqHxuLSgtQXPGNQ/Om7HWJ2siqmN6sm9mu5AN1TEXnDJZN73PLFEgjjSf5Tf4lhlb8KeVHO6qiYAOH9W6pFeuoa1/UQD21I+LSxf+oAmsh+VM9og8hQrGqibXDyrkK1rps9I8ULS3f76m/yHc3yl3qZ1TF8IaZ2J9CBGxH8h++vE/DrztyokGVRpLaW2zi6z0FpXGY/cHWHJa2vDDTY8dsz9 adamwick@ergates