├── .gitignore ├── assets ├── new_keys.raw ├── kex │ ├── dh-kex-gex │ │ ├── request.raw │ │ ├── group.raw │ │ ├── init.raw │ │ ├── reply.raw │ │ ├── client_kex_init.raw │ │ └── server_kex_init.raw │ ├── dh │ │ ├── init.raw │ │ ├── reply.raw │ │ ├── client_kex_init.raw │ │ └── server_kex_init.raw │ ├── ecdh │ │ ├── init.raw │ │ ├── reply.raw │ │ ├── client_kex_init.raw │ │ └── server_kex_init.raw │ └── kex-hybrid │ │ ├── ecdh-nistp256-kyber-512r3-sha256-d00 │ │ ├── init.raw │ │ ├── reply.raw │ │ ├── client_kex_init.raw │ │ └── server_kex_init.raw │ │ ├── ecdh-nistp384-kyber-768r3-sha384-d00 │ │ ├── init.raw │ │ ├── reply.raw │ │ ├── client_kex_init.raw │ │ └── server_kex_init.raw │ │ └── ecdh-nistp521-kyber-1024r3-sha512-d00 │ │ ├── init.raw │ │ ├── reply.raw │ │ ├── client_kex_init.raw │ │ └── server_kex_init.raw ├── client_init.raw └── server_compat.raw ├── .github ├── dependabot.yml └── workflows │ ├── security-audit.yml │ └── rust.yml ├── Cargo.toml ├── .pre-commit-config.yaml ├── src ├── lib.rs ├── mpint.rs ├── serialize.rs ├── ssh.rs └── kex.rs ├── LICENSE-MIT ├── README.md ├── tests ├── tests.rs └── tests_kex.rs ├── Cargo.lock └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | /.idea 3 | -------------------------------------------------------------------------------- /assets/new_keys.raw: -------------------------------------------------------------------------------- 1 | 2 |  -------------------------------------------------------------------------------- /assets/kex/dh-kex-gex/request.raw: -------------------------------------------------------------------------------- 1 | " -------------------------------------------------------------------------------- /assets/client_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/client_init.raw -------------------------------------------------------------------------------- /assets/kex/dh/init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/dh/init.raw -------------------------------------------------------------------------------- /assets/kex/dh/reply.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/dh/reply.raw -------------------------------------------------------------------------------- /assets/kex/ecdh/init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/ecdh/init.raw -------------------------------------------------------------------------------- /assets/kex/ecdh/reply.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/ecdh/reply.raw -------------------------------------------------------------------------------- /assets/server_compat.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/server_compat.raw -------------------------------------------------------------------------------- /assets/kex/dh-kex-gex/group.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/dh-kex-gex/group.raw -------------------------------------------------------------------------------- /assets/kex/dh-kex-gex/init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/dh-kex-gex/init.raw -------------------------------------------------------------------------------- /assets/kex/dh-kex-gex/reply.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/dh-kex-gex/reply.raw -------------------------------------------------------------------------------- /assets/kex/dh/client_kex_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/dh/client_kex_init.raw -------------------------------------------------------------------------------- /assets/kex/dh/server_kex_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/dh/server_kex_init.raw -------------------------------------------------------------------------------- /assets/kex/ecdh/client_kex_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/ecdh/client_kex_init.raw -------------------------------------------------------------------------------- /assets/kex/ecdh/server_kex_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/ecdh/server_kex_init.raw -------------------------------------------------------------------------------- /assets/kex/dh-kex-gex/client_kex_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/dh-kex-gex/client_kex_init.raw -------------------------------------------------------------------------------- /assets/kex/dh-kex-gex/server_kex_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/dh-kex-gex/server_kex_init.raw -------------------------------------------------------------------------------- /assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/init.raw -------------------------------------------------------------------------------- /assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/reply.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/reply.raw -------------------------------------------------------------------------------- /assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/init.raw -------------------------------------------------------------------------------- /assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/reply.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/reply.raw -------------------------------------------------------------------------------- /assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/init.raw -------------------------------------------------------------------------------- /assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/reply.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/reply.raw -------------------------------------------------------------------------------- /assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/client_kex_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/client_kex_init.raw -------------------------------------------------------------------------------- /assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/server_kex_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/server_kex_init.raw -------------------------------------------------------------------------------- /assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/client_kex_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/client_kex_init.raw -------------------------------------------------------------------------------- /assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/server_kex_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/server_kex_init.raw -------------------------------------------------------------------------------- /assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/client_kex_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/client_kex_init.raw -------------------------------------------------------------------------------- /assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/server_kex_init.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusticata/ssh-parser/HEAD/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/server_kex_init.raw -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: github-actions 8 | directory: "/" 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/security-audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | schedule: 4 | - cron: "0 8 * * *" 5 | push: 6 | paths: 7 | - "**/Cargo.*" 8 | jobs: 9 | security_audit: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v5 13 | - uses: rustsec/audit-check@v2 14 | with: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ssh-parser" 3 | version = "0.6.0" 4 | authors = ["Nicolas Vivet ", "Pierre Chifflier "] 5 | description = "Parser for the SSH protocol" 6 | license = "MIT OR Apache-2.0" 7 | homepage = "https://github.com/rusticata/ssh-parser" 8 | repository = "https://github.com/rusticata/ssh-parser" 9 | documentation = "https://docs.rs/ssh-parser" 10 | edition = "2018" 11 | rust-version = "1.63" 12 | 13 | [features] 14 | integers = ["num-bigint", "num-traits"] 15 | serialize = ["cookie-factory"] 16 | 17 | [dependencies] 18 | nom = "7.0" 19 | rusticata-macros = "4.0" 20 | 21 | cookie-factory = { version = "0.3", optional = true } 22 | num-bigint = { version = "0.4", optional = true } 23 | num-traits = { version = "0.2", optional = true } 24 | 25 | [package.metadata.cargo_check_external_types] 26 | allowed_external_types = [ 27 | "nom", 28 | "nom::*", 29 | ] 30 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | fail_fast: true 4 | 5 | exclude: | 6 | (?x)^( 7 | .*/(assets)/.*| 8 | )$ 9 | 10 | repos: 11 | - repo: 'https://github.com/pre-commit/pre-commit-hooks' 12 | rev: v6.0.0 13 | hooks: 14 | - id: trailing-whitespace 15 | - id: end-of-file-fixer 16 | - id: check-yaml 17 | - id: check-added-large-files 18 | 19 | - repo: local 20 | hooks: 21 | - id: cargo-fmt 22 | name: cargo fmt 23 | entry: cargo fmt -- 24 | language: system 25 | types: [rust] 26 | pass_filenames: false # This makes it a lot faster 27 | 28 | - id: cargo-clippy 29 | name: cargo clippy 30 | language: system 31 | types: [rust] 32 | pass_filenames: false 33 | entry: cargo clippy --all-targets --all-features -- -D warnings 34 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate is implemented using the parser combinator [nom](https://github.com/Geal/nom). 2 | //! 3 | //! The code is available on [GitHub](https://github.com/rusticata/ssh-parser) 4 | //! and is part of the [Rusticata](https://github.com/rusticata) project. 5 | 6 | pub mod kex; 7 | #[cfg(feature = "integers")] 8 | pub mod mpint; 9 | #[cfg(feature = "serialize")] 10 | /// SSH packet crafting functions 11 | pub mod serialize; 12 | mod ssh; 13 | 14 | pub use kex::{ 15 | ssh_kex_negociate_algorithm, ECDSASignature, SshKEX, SshKEXDiffieHellman, 16 | SshKEXDiffieHellmanKEXGEX, SshKEXECDiffieHellman, SshKEXError, SshPacketDHKEXInit, 17 | SshPacketDHKEXReply, SshPacketDhKEXGEXGroup, SshPacketDhKEXGEXInit, SshPacketDhKEXGEXReply, 18 | SshPacketDhKEXGEXRequest, SshPacketDhKEXGEXRequestOld, SshPacketECDHKEXInit, 19 | SshPacketECDHKEXReply, SshPacketHybridKEXInit, SshPacketHybridKEXReply, 20 | SupportedHybridKEXAlgorithm, 21 | }; 22 | pub use ssh::*; 23 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-20214 Nicolas Vivet, Pierre Chifflier 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Maintenance](https://img.shields.io/badge/maintenance-activly--developed-brightgreen.svg) 2 | [![LICENSE](https://img.shields.io/badge/License-LGPL%20v2.1-blue.svg)](LICENSE) 3 | [![Build Status](https://github.com/rusticata/ssh-parser/actions/workflows/rust.yml/badge.svg)](https://github.com/rusticata/ssh-parser/actions/workflows/rust.yml) 4 | [![Crates.io Version](https://img.shields.io/crates/v/ssh-parser.svg)](https://crates.io/crates/ssh-parser) 5 | 6 | # ssh-parser 7 | 8 | ## Overview 9 | 10 | This crate provides functions to parse the SSH 2.0 protocol packets. It is also 11 | able to recognize older versions of SSH in the identification phase. The main 12 | purpose of ssh-parser is to implement safe protocol analysis in network 13 | monitoring tools such as IDS and thus it is only able to parse unprotected 14 | packets (like the SSH handshake). 15 | 16 | ## Standards 17 | 18 | The following specification are partially implemented: 19 | - [RFC4253](https://tools.ietf.org/html/rfc4253) The Secure Shell (SSH) Transport Layer Protocol 20 | - [RFC4251](https://tools.ietf.org/html/rfc4251) The Secure Shell (SSH) Protocol Architecture 21 | - [RFC4250](https://tools.ietf.org/html/rfc4250) The Secure Shell (SSH) Protocol Assigned Numbers 22 | - [RFC5656](https://tools.ietf.org/html/rfc5656) Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer 23 | - [RFC6239](https://tools.ietf.org/html/rfc6239) Suite B Cryptographic Suites for Secure Shell (SSH) 24 | - [IANA SSH Protocol Parameters](http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xhtml) 25 | 26 | ## License 27 | 28 | This library is licensed under the GNU Lesser General Public License version 2.1, or (at your option) any later version. 29 | -------------------------------------------------------------------------------- /src/mpint.rs: -------------------------------------------------------------------------------- 1 | use nom::bits::{bits, streaming::take as btake}; 2 | use nom::error::Error; 3 | use nom::sequence::pair; 4 | use nom::IResult; 5 | use num_bigint::{BigInt, BigUint, Sign}; 6 | use num_traits::identities::Zero; 7 | use std::ops::{AddAssign, Shl, Shr}; 8 | 9 | struct MpUint(BigUint); 10 | 11 | impl AddAssign for MpUint { 12 | fn add_assign(&mut self, other: MpUint) { 13 | *self = MpUint(&self.0 + other.0); 14 | } 15 | } 16 | 17 | impl Shr for MpUint { 18 | type Output = MpUint; 19 | 20 | fn shr(self, shift: usize) -> MpUint { 21 | MpUint(&self.0 >> shift) 22 | } 23 | } 24 | 25 | impl Shl for MpUint { 26 | type Output = MpUint; 27 | 28 | fn shl(self, shift: usize) -> MpUint { 29 | MpUint(&self.0 << shift) 30 | } 31 | } 32 | 33 | impl From for MpUint { 34 | fn from(i: u8) -> MpUint { 35 | MpUint(BigUint::from(i)) 36 | } 37 | } 38 | 39 | pub fn parse_ssh_mpint(i: &[u8]) -> IResult<&[u8], BigInt> { 40 | if i.is_empty() { 41 | Ok((i, BigInt::zero())) 42 | } else { 43 | let (i, b) = bits(pair( 44 | btake::<_, _, _, Error<_>>(1usize), 45 | btake(i.len() * 8usize - 1), 46 | ))(i)?; 47 | let sign: u8 = b.0; 48 | let number = MpUint(b.1); 49 | let bi = BigInt::from_biguint(if sign == 0 { Sign::Plus } else { Sign::Minus }, number.0); 50 | Ok((i, bi)) 51 | } 52 | } 53 | 54 | #[test] 55 | fn test_positive_mpint() { 56 | let e = [ 57 | 0x04, 0xe7, 0x59, 0x2a, 0xe1, 0xb9, 0xb6, 0xbe, 0x7c, 0x81, 0x5f, 0xc8, 0x3d, 0x55, 0x7b, 58 | 0x8f, 0xc7, 0x09, 0x1d, 0x71, 0x6c, 0xed, 0x68, 0x45, 0x6c, 0x31, 0xc7, 0xf3, 0x65, 0x98, 59 | 0xa5, 0x44, 0x7d, 0xa4, 0x28, 0xdd, 0xe7, 0x3a, 0xd9, 0xa1, 0x0e, 0x4b, 0x75, 0x3a, 0xde, 60 | 0x33, 0x99, 0x6e, 0x41, 0x7d, 0xea, 0x88, 0xe9, 0x90, 0xe3, 0x5a, 0x27, 0xf8, 0x38, 0x09, 61 | 0x01, 0x66, 0x46, 0xd4, 0xdc, 62 | ]; 63 | let expected = Ok(( 64 | b"" as &[u8], 65 | BigInt::new( 66 | Sign::Plus, 67 | vec![ 68 | 1715918044, 4164421889, 2430818855, 2112522473, 865693249, 1265973982, 987341070, 69 | 2754141671, 2560967805, 835187557, 3983033708, 152924524, 1434161095, 2170538045, 70 | 3115761276, 3881380577, 4, 71 | ], 72 | ), 73 | )); 74 | let num = parse_ssh_mpint(&e); 75 | assert_eq!(num, expected); 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: 4 | push: 5 | pull_request: 6 | merge_group: 7 | schedule: 8 | - cron: '0 18 * * *' 9 | 10 | jobs: 11 | pre_commit: 12 | timeout-minutes: 5 13 | name: prek 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v5 17 | # tools used in pre-commit hooks 18 | # /tools 19 | - id: prek 20 | uses: rusticata/ci-action-prek@v1 21 | 22 | check: 23 | name: Check 24 | runs-on: ubuntu-latest 25 | strategy: 26 | matrix: 27 | rust: 28 | - stable 29 | - 1.63.0 30 | - nightly 31 | steps: 32 | - uses: actions/checkout@v5 33 | - name: Install ${{ matrix.rust }} toolchain 34 | uses: dtolnay/rust-toolchain@master 35 | with: 36 | toolchain: ${{ matrix.rust }} 37 | - name: Cargo update 38 | run: cargo update 39 | - run: RUSTFLAGS="-D warnings" cargo check 40 | 41 | test: 42 | name: Test Suite 43 | runs-on: ubuntu-latest 44 | strategy: 45 | matrix: 46 | features: 47 | - --no-default-features 48 | - --all-features 49 | - --features=serialize 50 | - --features=integers 51 | steps: 52 | - uses: actions/checkout@v5 53 | - name: Install stable toolchain 54 | uses: dtolnay/rust-toolchain@stable 55 | - run: cargo test ${{ matrix.features }} 56 | 57 | fmt: 58 | name: Rustfmt 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v5 62 | - name: Install stable rustfmt 63 | uses: dtolnay/rust-toolchain@stable 64 | with: 65 | components: rustfmt 66 | - run: cargo fmt --all -- --check 67 | 68 | clippy: 69 | name: Clippy 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v5 73 | - name: Install nightly clippy 74 | uses: dtolnay/rust-toolchain@nightly 75 | with: 76 | components: clippy 77 | - run: cargo clippy -- -D warnings 78 | 79 | doc: 80 | name: Build documentation 81 | runs-on: ubuntu-latest 82 | env: 83 | RUSTDOCFLAGS: --cfg docsrs 84 | steps: 85 | - uses: actions/checkout@v5 86 | - name: Install nightly rust 87 | uses: dtolnay/rust-toolchain@nightly 88 | - run: cargo doc --workspace --no-deps --all-features 89 | 90 | semver: 91 | name: Check semver compatibility 92 | runs-on: ubuntu-latest 93 | steps: 94 | - name: Checkout sources 95 | uses: actions/checkout@v5 96 | - name: Check semver 97 | uses: obi1kenobi/cargo-semver-checks-action@v2 98 | 99 | check-external-types: 100 | name: Validate external types appearing in public API 101 | runs-on: ubuntu-latest 102 | steps: 103 | - name: Checkout sources 104 | uses: actions/checkout@v5 105 | - id: cargo-rdme 106 | uses: rusticata/ci-action-check-external-types@v1 107 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | // Public API tests 2 | extern crate ssh_parser; 3 | 4 | use ssh_parser::*; 5 | 6 | static CLIENT_KEY_EXCHANGE: &[u8] = include_bytes!("../assets/client_init.raw"); 7 | static SERVER_COMPAT: &[u8] = include_bytes!("../assets/server_compat.raw"); 8 | 9 | #[test] 10 | fn test_identification() { 11 | let empty: Vec<&[u8]> = vec![]; 12 | let version = SshVersion { 13 | proto: b"2.0", 14 | software: b"OpenSSH_7.3", 15 | comments: None, 16 | }; 17 | 18 | let expected = Ok((b"" as &[u8], (empty, version))); 19 | let res = parse_ssh_identification(&CLIENT_KEY_EXCHANGE[..21]); 20 | assert_eq!(res, expected); 21 | } 22 | 23 | #[test] 24 | fn test_compatibility() { 25 | let empty: Vec<&[u8]> = vec![]; 26 | let version = SshVersion { 27 | proto: b"1.99", 28 | software: b"OpenSSH_3.1p1", 29 | comments: None, 30 | }; 31 | 32 | let expected = Ok((b"" as &[u8], (empty, version))); 33 | let res = parse_ssh_identification(&SERVER_COMPAT[..23]); 34 | assert_eq!(res, expected); 35 | } 36 | 37 | #[test] 38 | fn test_version_with_comments() { 39 | let empty: Vec<&[u8]> = vec![]; 40 | let version = SshVersion { 41 | proto: b"2.0", 42 | software: b"OpenSSH_7.3", 43 | comments: Some(b"toto"), 44 | }; 45 | let expected = Ok((b"" as &[u8], (empty, version))); 46 | let res = parse_ssh_identification(b"SSH-2.0-OpenSSH_7.3 toto\r\n"); 47 | assert_eq!(res, expected); 48 | } 49 | 50 | #[test] 51 | fn test_client_key_exchange() { 52 | let cookie = [ 53 | 0xca, 0x98, 0x42, 0x14, 0xd6, 0xa5, 0xa7, 0xfd, 0x6c, 0xe8, 0xd4, 0x7c, 0x0b, 0xc0, 0x96, 54 | 0xcc, 55 | ]; 56 | let key_exchange = SshPacket::KeyExchange(SshPacketKeyExchange { 57 | cookie: &cookie, 58 | kex_algs: b"curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,ext-info-c", 59 | server_host_key_algs: b"ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa", 60 | encr_algs_client_to_server: b"chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc,3des-cbc", 61 | encr_algs_server_to_client: b"chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc,3des-cbc", 62 | mac_algs_client_to_server: b"umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", 63 | mac_algs_server_to_client: b"umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", 64 | comp_algs_client_to_server: b"none,zlib@openssh.com,zlib", 65 | comp_algs_server_to_client: b"none,zlib@openssh.com,zlib", 66 | langs_client_to_server: b"", 67 | langs_server_to_client: b"", 68 | first_kex_packet_follows: false, 69 | }); 70 | let padding: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 71 | 72 | let expected = Ok((b"" as &[u8], (key_exchange, padding))); 73 | let res = parse_ssh_packet(&CLIENT_KEY_EXCHANGE[21..]); 74 | assert_eq!(res, expected); 75 | } 76 | -------------------------------------------------------------------------------- /src/serialize.rs: -------------------------------------------------------------------------------- 1 | use super::{SshPacket, SshPacketDebug, SshPacketDisconnect, SshPacketKeyExchange}; 2 | use cookie_factory::gen::{set_be_u32, set_be_u8}; 3 | use cookie_factory::*; 4 | use std::iter::repeat; 5 | 6 | fn gen_string<'a>(x: (&'a mut [u8], usize), s: &[u8]) -> Result<(&'a mut [u8], usize), GenError> { 7 | do_gen!(x, gen_be_u32!(s.len() as u32) >> gen_slice!(s)) 8 | } 9 | 10 | fn gen_packet_key_exchange<'a>( 11 | x: (&'a mut [u8], usize), 12 | p: &SshPacketKeyExchange, 13 | ) -> Result<(&'a mut [u8], usize), GenError> { 14 | do_gen!( 15 | x, 16 | gen_copy!(p.cookie, 16) 17 | >> gen_string(p.kex_algs) 18 | >> gen_string(p.server_host_key_algs) 19 | >> gen_string(p.encr_algs_client_to_server) 20 | >> gen_string(p.encr_algs_server_to_client) 21 | >> gen_string(p.mac_algs_client_to_server) 22 | >> gen_string(p.mac_algs_server_to_client) 23 | >> gen_string(p.comp_algs_client_to_server) 24 | >> gen_string(p.comp_algs_server_to_client) 25 | >> gen_string(p.langs_client_to_server) 26 | >> gen_string(p.langs_server_to_client) 27 | >> gen_be_u8!(if p.first_kex_packet_follows { 1 } else { 0 }) 28 | >> gen_be_u32!(0) 29 | ) 30 | } 31 | 32 | fn gen_packet_disconnect<'a>( 33 | x: (&'a mut [u8], usize), 34 | p: &SshPacketDisconnect, 35 | ) -> Result<(&'a mut [u8], usize), GenError> { 36 | do_gen!( 37 | x, 38 | gen_be_u32!(p.reason_code) >> gen_string(p.description) >> gen_string(p.lang) 39 | ) 40 | } 41 | 42 | fn gen_packet_debug<'a>( 43 | x: (&'a mut [u8], usize), 44 | p: &SshPacketDebug, 45 | ) -> Result<(&'a mut [u8], usize), GenError> { 46 | do_gen!( 47 | x, 48 | gen_be_u8!(if p.always_display { 1 } else { 0 }) 49 | >> gen_string(p.message) 50 | >> gen_string(p.lang) 51 | ) 52 | } 53 | 54 | fn packet_payload_type(p: &SshPacket) -> u8 { 55 | match *p { 56 | SshPacket::Disconnect(_) => 1, 57 | SshPacket::Ignore(_) => 2, 58 | SshPacket::Unimplemented(_) => 3, 59 | SshPacket::Debug(_) => 4, 60 | SshPacket::ServiceRequest(_) => 5, 61 | SshPacket::ServiceAccept(_) => 6, 62 | SshPacket::KeyExchange(_) => 20, 63 | SshPacket::NewKeys => 21, 64 | SshPacket::DiffieHellmanKEX(ref p) => p.0.message_code, 65 | } 66 | } 67 | 68 | fn gen_packet_payload<'a>( 69 | x: (&'a mut [u8], usize), 70 | p: &SshPacket, 71 | ) -> Result<(&'a mut [u8], usize), GenError> { 72 | match *p { 73 | SshPacket::Disconnect(ref p) => gen_packet_disconnect(x, p), 74 | SshPacket::Ignore(p) => gen_string(x, p), 75 | SshPacket::Unimplemented(n) => set_be_u32(x, n), 76 | SshPacket::Debug(ref p) => gen_packet_debug(x, p), 77 | SshPacket::ServiceRequest(p) => gen_string(x, p), 78 | SshPacket::ServiceAccept(p) => gen_string(x, p), 79 | SshPacket::KeyExchange(ref p) => gen_packet_key_exchange(x, p), 80 | SshPacket::NewKeys => Ok(x), 81 | SshPacket::DiffieHellmanKEX(ref p) => gen_string(x, p.0.payload), 82 | } 83 | } 84 | 85 | fn padding_len(payload: usize) -> usize { 86 | let len = 8 - (payload % 8); 87 | 88 | if len < 4 { 89 | len + 8 90 | } else { 91 | len 92 | } 93 | } 94 | 95 | /// Serialize an SSH packet from its intermediate representation. 96 | pub fn gen_ssh_packet<'a>( 97 | x: (&'a mut [u8], usize), 98 | p: &SshPacket, 99 | ) -> Result<(&'a mut [u8], usize), GenError> { 100 | do_gen!( 101 | x, 102 | len: gen_skip!(4) 103 | >> padlen: gen_skip!(1) 104 | >> gen_be_u8!(packet_payload_type(p)) 105 | >> gen_packet_payload(p) 106 | >> pad: gen_many!(repeat(0).take(padding_len(pad - len)), set_be_u8) 107 | >> end: gen_at_offset!(padlen, gen_be_u8!((end - pad) as u8)) 108 | >> gen_at_offset!(len, gen_be_u32!((end - padlen) as u32)) 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.2.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" 10 | 11 | [[package]] 12 | name = "cookie-factory" 13 | version = "0.3.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" 16 | dependencies = [ 17 | "futures", 18 | ] 19 | 20 | [[package]] 21 | name = "futures" 22 | version = "0.3.30" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 25 | dependencies = [ 26 | "futures-channel", 27 | "futures-core", 28 | "futures-executor", 29 | "futures-io", 30 | "futures-sink", 31 | "futures-task", 32 | "futures-util", 33 | ] 34 | 35 | [[package]] 36 | name = "futures-channel" 37 | version = "0.3.30" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 40 | dependencies = [ 41 | "futures-core", 42 | "futures-sink", 43 | ] 44 | 45 | [[package]] 46 | name = "futures-core" 47 | version = "0.3.30" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 50 | 51 | [[package]] 52 | name = "futures-executor" 53 | version = "0.3.30" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 56 | dependencies = [ 57 | "futures-core", 58 | "futures-task", 59 | "futures-util", 60 | ] 61 | 62 | [[package]] 63 | name = "futures-io" 64 | version = "0.3.30" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 67 | 68 | [[package]] 69 | name = "futures-macro" 70 | version = "0.3.30" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 73 | dependencies = [ 74 | "proc-macro2", 75 | "quote", 76 | "syn", 77 | ] 78 | 79 | [[package]] 80 | name = "futures-sink" 81 | version = "0.3.30" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 84 | 85 | [[package]] 86 | name = "futures-task" 87 | version = "0.3.30" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 90 | 91 | [[package]] 92 | name = "futures-util" 93 | version = "0.3.30" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 96 | dependencies = [ 97 | "futures-channel", 98 | "futures-core", 99 | "futures-io", 100 | "futures-macro", 101 | "futures-sink", 102 | "futures-task", 103 | "memchr", 104 | "pin-project-lite", 105 | "pin-utils", 106 | "slab", 107 | ] 108 | 109 | [[package]] 110 | name = "memchr" 111 | version = "2.7.2" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 114 | 115 | [[package]] 116 | name = "minimal-lexical" 117 | version = "0.2.1" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 120 | 121 | [[package]] 122 | name = "nom" 123 | version = "7.1.3" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 126 | dependencies = [ 127 | "memchr", 128 | "minimal-lexical", 129 | ] 130 | 131 | [[package]] 132 | name = "num-bigint" 133 | version = "0.4.4" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" 136 | dependencies = [ 137 | "autocfg", 138 | "num-integer", 139 | "num-traits", 140 | ] 141 | 142 | [[package]] 143 | name = "num-integer" 144 | version = "0.1.46" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 147 | dependencies = [ 148 | "num-traits", 149 | ] 150 | 151 | [[package]] 152 | name = "num-traits" 153 | version = "0.2.18" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 156 | dependencies = [ 157 | "autocfg", 158 | ] 159 | 160 | [[package]] 161 | name = "pin-project-lite" 162 | version = "0.2.14" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 165 | 166 | [[package]] 167 | name = "pin-utils" 168 | version = "0.1.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 171 | 172 | [[package]] 173 | name = "proc-macro2" 174 | version = "1.0.79" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" 177 | dependencies = [ 178 | "unicode-ident", 179 | ] 180 | 181 | [[package]] 182 | name = "quote" 183 | version = "1.0.35" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 186 | dependencies = [ 187 | "proc-macro2", 188 | ] 189 | 190 | [[package]] 191 | name = "rusticata-macros" 192 | version = "4.1.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" 195 | dependencies = [ 196 | "nom", 197 | ] 198 | 199 | [[package]] 200 | name = "slab" 201 | version = "0.4.9" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 204 | dependencies = [ 205 | "autocfg", 206 | ] 207 | 208 | [[package]] 209 | name = "ssh-parser" 210 | version = "0.6.0" 211 | dependencies = [ 212 | "cookie-factory", 213 | "nom", 214 | "num-bigint", 215 | "num-traits", 216 | "rusticata-macros", 217 | ] 218 | 219 | [[package]] 220 | name = "syn" 221 | version = "2.0.58" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" 224 | dependencies = [ 225 | "proc-macro2", 226 | "quote", 227 | "unicode-ident", 228 | ] 229 | 230 | [[package]] 231 | name = "unicode-ident" 232 | version = "1.0.12" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 235 | -------------------------------------------------------------------------------- /tests/tests_kex.rs: -------------------------------------------------------------------------------- 1 | // Public API tests for KEX. 2 | extern crate ssh_parser; 3 | 4 | use ssh_parser::*; 5 | 6 | fn load_client_server_key_exchange_init( 7 | client: &'static [u8], 8 | server: &'static [u8], 9 | ) -> (SshPacketKeyExchange<'static>, SshPacketKeyExchange<'static>) { 10 | let client = parse_ssh_packet(client).unwrap().1 .0; 11 | let server = parse_ssh_packet(server).unwrap().1 .0; 12 | assert!(matches!( 13 | (&client, &server), 14 | (SshPacket::KeyExchange(_), SshPacket::KeyExchange(_)) 15 | )); 16 | match (client, server) { 17 | (SshPacket::KeyExchange(client), SshPacket::KeyExchange(server)) => (client, server), 18 | _ => unreachable!(), 19 | } 20 | } 21 | 22 | fn load_kex_packet(packet: &[u8]) -> SshPacketUnparsed<'_> { 23 | let kex_packet = parse_ssh_packet(packet).unwrap().1 .0; 24 | assert!(matches!(&kex_packet, SshPacket::DiffieHellmanKEX(_))); 25 | match kex_packet { 26 | SshPacket::DiffieHellmanKEX(kex) => kex.0, 27 | _ => unreachable!(), 28 | } 29 | } 30 | 31 | mod ecdh { 32 | use super::*; 33 | 34 | static CLIENT_KEY_EXCHANGE_INIT: &[u8] = 35 | include_bytes!("../assets/kex/ecdh/client_kex_init.raw"); 36 | static SERVER_KEY_EXCHANGE_INIT: &[u8] = 37 | include_bytes!("../assets/kex/ecdh/server_kex_init.raw"); 38 | static INIT: &[u8] = include_bytes!("../assets/kex/ecdh/init.raw"); 39 | static REPLY: &[u8] = include_bytes!("../assets/kex/ecdh/reply.raw"); 40 | 41 | #[test] 42 | fn test_kex() { 43 | let (client_kex, server_kex) = load_client_server_key_exchange_init( 44 | CLIENT_KEY_EXCHANGE_INIT, 45 | SERVER_KEY_EXCHANGE_INIT, 46 | ); 47 | let (mut kex, negociated_alg) = SshKEX::init(&client_kex, &server_kex).unwrap(); 48 | assert_eq!(negociated_alg, "curve25519-sha256"); 49 | assert!(matches!(kex, SshKEX::ECDiffieHellman(_))); 50 | 51 | let init_packet = load_kex_packet(INIT); 52 | assert!(matches!(kex.parse_ssh_packet(&init_packet), Ok(()))); 53 | assert!(matches!( 54 | kex.parse_ssh_packet(&init_packet), 55 | Err(SshKEXError::DuplicatedMessage) 56 | )); 57 | 58 | let reply_packet = load_kex_packet(REPLY); 59 | assert!(matches!(kex.parse_ssh_packet(&reply_packet), Ok(()))); 60 | assert!(matches!( 61 | kex.parse_ssh_packet(&reply_packet), 62 | Err(SshKEXError::DuplicatedMessage) 63 | )); 64 | 65 | let kex = match kex { 66 | SshKEX::ECDiffieHellman(kex) => kex, 67 | _ => unreachable!(), 68 | }; 69 | 70 | assert!(kex.init.is_some()); 71 | assert!(kex.reply.is_some()); 72 | } 73 | } 74 | 75 | mod dh { 76 | use super::*; 77 | 78 | static CLIENT_KEY_EXCHANGE_INIT: &[u8] = include_bytes!("../assets/kex/dh/client_kex_init.raw"); 79 | static SERVER_KEY_EXCHANGE_INIT: &[u8] = include_bytes!("../assets/kex/dh/server_kex_init.raw"); 80 | static INIT: &[u8] = include_bytes!("../assets/kex/dh/init.raw"); 81 | static REPLY: &[u8] = include_bytes!("../assets/kex/dh/reply.raw"); 82 | 83 | #[test] 84 | fn test_kex() { 85 | let (client_kex, server_kex) = load_client_server_key_exchange_init( 86 | CLIENT_KEY_EXCHANGE_INIT, 87 | SERVER_KEY_EXCHANGE_INIT, 88 | ); 89 | let (mut kex, negociated_alg) = SshKEX::init(&client_kex, &server_kex).unwrap(); 90 | assert_eq!(negociated_alg, "diffie-hellman-group18-sha512"); 91 | assert!(matches!(kex, SshKEX::DiffieHellman(_))); 92 | 93 | let init_packet = load_kex_packet(INIT); 94 | assert!(matches!(kex.parse_ssh_packet(&init_packet), Ok(()))); 95 | assert!(matches!( 96 | kex.parse_ssh_packet(&init_packet), 97 | Err(SshKEXError::DuplicatedMessage) 98 | )); 99 | 100 | let reply_packet = load_kex_packet(REPLY); 101 | assert!(matches!(kex.parse_ssh_packet(&reply_packet), Ok(()))); 102 | assert!(matches!( 103 | kex.parse_ssh_packet(&reply_packet), 104 | Err(SshKEXError::DuplicatedMessage) 105 | )); 106 | 107 | let kex = match kex { 108 | SshKEX::DiffieHellman(kex) => kex, 109 | _ => unreachable!(), 110 | }; 111 | 112 | assert!(kex.init.is_some()); 113 | assert!(kex.reply.is_some()); 114 | 115 | let ecdsa_signature = kex.reply.as_ref().unwrap().get_ecdsa_signature().unwrap(); 116 | assert_eq!(ecdsa_signature.identifier, "ssh-ed25519"); 117 | } 118 | } 119 | 120 | mod dh_kex_gex { 121 | use super::*; 122 | 123 | static CLIENT_KEY_EXCHANGE_INIT: &[u8] = 124 | include_bytes!("../assets/kex/dh-kex-gex/client_kex_init.raw"); 125 | static SERVER_KEY_EXCHANGE_INIT: &[u8] = 126 | include_bytes!("../assets/kex/dh-kex-gex/server_kex_init.raw"); 127 | static REQUEST: &[u8] = include_bytes!("../assets/kex/dh-kex-gex/request.raw"); 128 | static GROUP: &[u8] = include_bytes!("../assets/kex/dh-kex-gex/group.raw"); 129 | static INIT: &[u8] = include_bytes!("../assets/kex/dh-kex-gex/init.raw"); 130 | static REPLY: &[u8] = include_bytes!("../assets/kex/dh-kex-gex/reply.raw"); 131 | 132 | #[test] 133 | fn test_kex() { 134 | let (client_kex, server_kex) = load_client_server_key_exchange_init( 135 | CLIENT_KEY_EXCHANGE_INIT, 136 | SERVER_KEY_EXCHANGE_INIT, 137 | ); 138 | let (mut kex, negociated_alg) = SshKEX::init(&client_kex, &server_kex).unwrap(); 139 | assert_eq!(negociated_alg, "diffie-hellman-group-exchange-sha256"); 140 | assert!(matches!(kex, SshKEX::DiffieHellmanKEXGEX(_))); 141 | 142 | let request_packet = load_kex_packet(REQUEST); 143 | assert!(matches!(kex.parse_ssh_packet(&request_packet), Ok(()))); 144 | assert!(matches!( 145 | kex.parse_ssh_packet(&request_packet), 146 | Err(SshKEXError::DuplicatedMessage) 147 | )); 148 | 149 | let group_packet = load_kex_packet(GROUP); 150 | assert!(matches!(kex.parse_ssh_packet(&group_packet), Ok(()))); 151 | assert!(matches!( 152 | kex.parse_ssh_packet(&group_packet), 153 | Err(SshKEXError::DuplicatedMessage) 154 | )); 155 | 156 | let init_packet = load_kex_packet(INIT); 157 | assert!(matches!(kex.parse_ssh_packet(&init_packet), Ok(()))); 158 | assert!(matches!( 159 | kex.parse_ssh_packet(&init_packet), 160 | Err(SshKEXError::DuplicatedMessage) 161 | )); 162 | 163 | let reply_packet = load_kex_packet(REPLY); 164 | assert!(matches!(kex.parse_ssh_packet(&reply_packet), Ok(()))); 165 | assert!(matches!( 166 | kex.parse_ssh_packet(&reply_packet), 167 | Err(SshKEXError::DuplicatedMessage) 168 | )); 169 | 170 | let kex = match kex { 171 | SshKEX::DiffieHellmanKEXGEX(kex) => kex, 172 | _ => unreachable!(), 173 | }; 174 | 175 | assert!(kex.request.is_some()); 176 | assert!(kex.group.is_some()); 177 | assert!(kex.init.is_some()); 178 | assert!(kex.reply.is_some()); 179 | } 180 | } 181 | 182 | mod kex_hybrid_oqs { 183 | use std::fs; 184 | use std::path::Path; 185 | 186 | use super::*; 187 | 188 | /// Path to assets. 189 | const ASSETS_PATH: &str = "assets/kex/kex-hybrid"; 190 | 191 | fn read_test_file(directory: &Path, filename: &str) -> &'static [u8] { 192 | let data = Box::new(fs::read(directory.join(filename)).unwrap()); 193 | Box::leak(data) 194 | } 195 | 196 | /// Tests an hybrid algorithm with a directory containing its assets. 197 | fn test_alg_with_directory(directory: impl AsRef, expected_algorithm: impl AsRef) { 198 | let directory = Path::new(ASSETS_PATH).join(directory); 199 | println!("dir={}", directory.display()); 200 | let client_kex_init = read_test_file(&directory, "client_kex_init.raw"); 201 | let server_kex_init = read_test_file(&directory, "server_kex_init.raw"); 202 | 203 | let init_msg = fs::read(directory.join("init.raw")).unwrap(); 204 | let reply_msg = fs::read(directory.join("reply.raw")).unwrap(); 205 | 206 | let (client_kex, server_kex) = 207 | load_client_server_key_exchange_init(client_kex_init, server_kex_init); 208 | 209 | let (mut kex, negotiated_alg) = SshKEX::init(&client_kex, &server_kex).unwrap(); 210 | assert_eq!(negotiated_alg, expected_algorithm.as_ref()); 211 | assert!(matches!(kex, SshKEX::HybridKEX(_))); 212 | 213 | let init_packet = load_kex_packet(&init_msg); 214 | assert!(matches!(kex.parse_ssh_packet(&init_packet), Ok(()))); 215 | assert!(matches!( 216 | kex.parse_ssh_packet(&init_packet), 217 | Err(SshKEXError::DuplicatedMessage) 218 | )); 219 | 220 | let reply_packet = load_kex_packet(&reply_msg); 221 | assert!(matches!(kex.parse_ssh_packet(&reply_packet), Ok(()))); 222 | assert!(matches!( 223 | kex.parse_ssh_packet(&reply_packet), 224 | Err(SshKEXError::DuplicatedMessage) 225 | )); 226 | 227 | let kex = match kex { 228 | SshKEX::HybridKEX(kex) => kex, 229 | _ => unreachable!(), 230 | }; 231 | 232 | assert!(kex.init.is_some()); 233 | assert!(kex.reply.is_some()); 234 | } 235 | 236 | #[test] 237 | fn ecdh_nistp256_kyber_512r3_sha256_d00_openquantumsafe_org_test() { 238 | test_alg_with_directory( 239 | "ecdh-nistp256-kyber-512r3-sha256-d00", 240 | "ecdh-nistp256-kyber-512r3-sha256-d00@openquantumsafe.org", 241 | ); 242 | } 243 | 244 | #[test] 245 | fn ecdh_nistp384_kyber_768r3_sha384_d00_openquantumsafe_org_test() { 246 | test_alg_with_directory( 247 | "ecdh-nistp384-kyber-768r3-sha384-d00", 248 | "ecdh-nistp384-kyber-768r3-sha384-d00@openquantumsafe.org", 249 | ); 250 | } 251 | 252 | #[test] 253 | fn ecdh_nistp521_kyber_1024r3_sha512_d00_openquantumsafe_org_test() { 254 | test_alg_with_directory( 255 | "ecdh-nistp521-kyber-1024r3-sha512-d00", 256 | "ecdh-nistp521-kyber-1024r3-sha512-d00@openquantumsafe.org", 257 | ); 258 | } 259 | } 260 | 261 | mod kex_algorithm_negociation { 262 | use super::ssh_kex_negociate_algorithm; 263 | 264 | #[test] 265 | fn test_negociation() { 266 | assert_eq!( 267 | ssh_kex_negociate_algorithm(["a", "b", "c"], ["a", "b", "c"]), 268 | Some("a") 269 | ); 270 | assert_eq!( 271 | ssh_kex_negociate_algorithm(["a", "b", "c"], ["b", "a", "c"]), 272 | Some("a") 273 | ); 274 | assert_eq!( 275 | ssh_kex_negociate_algorithm(["a", "b", "c"], ["b", "d", "c"]), 276 | Some("b") 277 | ); 278 | assert_eq!( 279 | ssh_kex_negociate_algorithm(["a", "b", "c"], ["d", "c", "e"]), 280 | Some("c") 281 | ); 282 | assert_eq!( 283 | ssh_kex_negociate_algorithm(["a", "b", "c"], ["c", "b", "a"]), 284 | Some("a") 285 | ); 286 | assert_eq!( 287 | ssh_kex_negociate_algorithm(["a", "b", "c"], ["d", "e", "f"]), 288 | None 289 | ); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/ssh.rs: -------------------------------------------------------------------------------- 1 | //! # SSH parser 2 | //! 3 | //! This module contains parsing functions for the SSH 2.0 protocol. It is also 4 | //! compatible with obsolete version negotiation. 5 | 6 | use nom::bytes::streaming::{is_not, tag, take, take_until}; 7 | use nom::character::streaming::{crlf, line_ending, not_line_ending}; 8 | use nom::combinator::{complete, map, map_res, opt}; 9 | use nom::error::{make_error, Error, ErrorKind}; 10 | use nom::multi::{length_data, many_till, separated_list1}; 11 | use nom::number::streaming::{be_u32, be_u8}; 12 | use nom::sequence::{delimited, terminated}; 13 | use nom::{Err, IResult}; 14 | use rusticata_macros::newtype_enum; 15 | use std::str; 16 | 17 | /// SSH Protocol Version Exchange 18 | /// 19 | /// Defined in [RFC4253 section 4.2](https://tools.ietf.org/html/rfc4253#section-4.2). 20 | /// 21 | /// Unparsed proto and software fields must contain US-ASCII printable 22 | /// characters only (without space and minus sign). There is no constraint on 23 | /// the comment field except it must not contain the null byte. 24 | #[derive(Debug, Eq, PartialEq)] 25 | pub struct SshVersion<'a> { 26 | pub proto: &'a [u8], 27 | pub software: &'a [u8], 28 | pub comments: Option<&'a [u8]>, 29 | } 30 | 31 | // Version exchange terminates with CRLF for SSH 2.0 or LF for compatibility 32 | // with older versions. 33 | fn parse_version(i: &[u8]) -> IResult<&[u8], SshVersion<'_>> { 34 | let (i, proto) = take_until("-")(i)?; 35 | let (i, _) = tag("-")(i)?; 36 | let (i, software) = is_not(" \r\n")(i)?; 37 | let (i, comments) = opt(|d| { 38 | let (d, _) = tag(" ")(d)?; 39 | let (d, comments) = not_line_ending(d)?; 40 | Ok((d, comments)) 41 | })(i)?; 42 | let version = SshVersion { 43 | proto, 44 | software, 45 | comments, 46 | }; 47 | Ok((i, version)) 48 | } 49 | 50 | /// Parse the SSH identification phase. 51 | /// 52 | /// In version 2.0, the SSH server is allowed to send an arbitrary number of 53 | /// UTF-8 lines before the final identification line containing the server 54 | /// version. This function allocates a vector to store these line slices in 55 | /// addition of the advertised version of the SSH implementation. 56 | pub fn parse_ssh_identification(i: &[u8]) -> IResult<&[u8], (Vec<&[u8]>, SshVersion<'_>)> { 57 | many_till( 58 | terminated(take_until("\r\n"), crlf), 59 | delimited(tag("SSH-"), parse_version, line_ending), 60 | )(i) 61 | } 62 | 63 | #[inline] 64 | pub(super) fn parse_string(i: &[u8]) -> IResult<&[u8], &[u8]> { 65 | length_data(be_u32)(i) 66 | } 67 | 68 | // US-ASCII printable characters without comma 69 | #[inline] 70 | fn is_us_ascii(c: u8) -> bool { 71 | (0x20..=0x7e).contains(&c) && c != 0x2c 72 | } 73 | 74 | #[inline] 75 | fn parse_name(s: &[u8]) -> IResult<&[u8], &[u8]> { 76 | use nom::bytes::complete::take_while1; 77 | take_while1(is_us_ascii)(s) 78 | } 79 | 80 | fn parse_name_list(i: &[u8]) -> IResult<&[u8], Vec<&str>> { 81 | use nom::bytes::complete::tag; 82 | match separated_list1(tag(","), map_res(complete(parse_name), str::from_utf8))(i) { 83 | Ok((rem, res)) => Ok((rem, res)), 84 | Err(_) => Err(Err::Error(make_error(i, ErrorKind::SeparatedList))), 85 | } 86 | } 87 | 88 | /// Return the second component of a pair. 89 | fn snd(tuple: (A, B)) -> B { 90 | tuple.1 91 | } 92 | 93 | /// SSH Algorithm Negotiation 94 | /// 95 | /// Defined in [RFC4253 section 7.1](https://tools.ietf.org/html/rfc4253#section-7.1). 96 | /// 97 | /// This packet contains all information necessary to prepare the key exchange. 98 | /// The algorithms are UTF-8 strings in name lists. The order is significant 99 | /// with most preferred algorithms first. Parsing of lists is done only when 100 | /// the field are accessed though accessors (note that lists can 101 | /// be successfully extracted at the packet level but accessing them later can 102 | /// fail with a UTF-8 conversion error). 103 | #[derive(Debug, Eq, PartialEq)] 104 | pub struct SshPacketKeyExchange<'a> { 105 | pub cookie: &'a [u8], 106 | pub kex_algs: &'a [u8], 107 | pub server_host_key_algs: &'a [u8], 108 | pub encr_algs_client_to_server: &'a [u8], 109 | pub encr_algs_server_to_client: &'a [u8], 110 | pub mac_algs_client_to_server: &'a [u8], 111 | pub mac_algs_server_to_client: &'a [u8], 112 | pub comp_algs_client_to_server: &'a [u8], 113 | pub comp_algs_server_to_client: &'a [u8], 114 | pub langs_client_to_server: &'a [u8], 115 | pub langs_server_to_client: &'a [u8], 116 | pub first_kex_packet_follows: bool, 117 | } 118 | 119 | fn parse_packet_key_exchange(i: &[u8]) -> IResult<&[u8], SshPacket<'_>> { 120 | let (i, cookie) = take(16usize)(i)?; 121 | let (i, kex_algs) = parse_string(i)?; 122 | let (i, server_host_key_algs) = parse_string(i)?; 123 | let (i, encr_algs_client_to_server) = parse_string(i)?; 124 | let (i, encr_algs_server_to_client) = parse_string(i)?; 125 | let (i, mac_algs_client_to_server) = parse_string(i)?; 126 | let (i, mac_algs_server_to_client) = parse_string(i)?; 127 | let (i, comp_algs_client_to_server) = parse_string(i)?; 128 | let (i, comp_algs_server_to_client) = parse_string(i)?; 129 | let (i, langs_client_to_server) = parse_string(i)?; 130 | let (i, langs_server_to_client) = parse_string(i)?; 131 | let (i, first_kex_packet_follows) = be_u8(i)?; 132 | let (i, _) = be_u32(i)?; 133 | let packet = SshPacketKeyExchange { 134 | cookie, 135 | kex_algs, 136 | server_host_key_algs, 137 | encr_algs_client_to_server, 138 | encr_algs_server_to_client, 139 | mac_algs_client_to_server, 140 | mac_algs_server_to_client, 141 | comp_algs_client_to_server, 142 | comp_algs_server_to_client, 143 | langs_client_to_server, 144 | langs_server_to_client, 145 | first_kex_packet_follows: first_kex_packet_follows > 0, 146 | }; 147 | Ok((i, SshPacket::KeyExchange(packet))) 148 | } 149 | 150 | impl<'a> SshPacketKeyExchange<'a> { 151 | pub fn get_kex_algs(&self) -> Result, nom::Err>> { 152 | parse_name_list(self.kex_algs).map(snd) 153 | } 154 | 155 | pub fn get_server_host_key_algs(&self) -> Result, nom::Err>> { 156 | parse_name_list(self.server_host_key_algs).map(snd) 157 | } 158 | 159 | pub fn get_encr_algs_client_to_server(&self) -> Result, nom::Err>> { 160 | parse_name_list(self.encr_algs_client_to_server).map(snd) 161 | } 162 | 163 | pub fn get_encr_algs_server_to_client(&self) -> Result, nom::Err>> { 164 | parse_name_list(self.encr_algs_server_to_client).map(snd) 165 | } 166 | 167 | pub fn get_mac_algs_client_to_server(&self) -> Result, nom::Err>> { 168 | parse_name_list(self.mac_algs_client_to_server).map(snd) 169 | } 170 | 171 | pub fn get_mac_algs_server_to_client(&self) -> Result, nom::Err>> { 172 | parse_name_list(self.mac_algs_server_to_client).map(snd) 173 | } 174 | 175 | pub fn get_comp_algs_client_to_server(&self) -> Result, nom::Err>> { 176 | parse_name_list(self.comp_algs_client_to_server).map(snd) 177 | } 178 | 179 | pub fn get_comp_algs_server_to_client(&self) -> Result, nom::Err>> { 180 | parse_name_list(self.comp_algs_server_to_client).map(snd) 181 | } 182 | 183 | pub fn get_langs_client_to_server(&self) -> Result, nom::Err>> { 184 | parse_name_list(self.langs_client_to_server).map(snd) 185 | } 186 | 187 | pub fn get_langs_server_to_client(&self) -> Result, nom::Err>> { 188 | parse_name_list(self.langs_server_to_client).map(snd) 189 | } 190 | } 191 | 192 | /// SSH Disconnection Message 193 | /// 194 | /// Defined in [RFC4253 Section 11.1](https://tools.ietf.org/html/rfc4253#section-11.1). 195 | #[derive(Debug, Eq, PartialEq)] 196 | pub struct SshPacketDisconnect<'a> { 197 | pub reason_code: u32, 198 | pub description: &'a [u8], 199 | pub lang: &'a [u8], 200 | } 201 | 202 | /// SSH Disconnection Message Reason Code 203 | /// 204 | /// Defined in [IANA SSH Protocol Parameters](http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xhtml#ssh-parameters-3). 205 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 206 | pub struct SshDisconnectReason(pub u32); 207 | 208 | newtype_enum! { 209 | impl display SshDisconnectReason { 210 | HostNotAllowedToConnect = 1, 211 | ProtocolError = 2, 212 | KeyExchangeFailed = 3, 213 | Reserved = 4, 214 | MacError = 5, 215 | CompressionError = 6, 216 | ServiceNotAvailable = 7, 217 | ProtocolVersionNotSupported = 8, 218 | HostKeyNotVerifiable = 9, 219 | ConnectionLost = 10, 220 | ByApplication = 11, 221 | TooManyConnections = 12, 222 | AuthCancelledByUser = 13, 223 | NoMoreAuthMethodsAvailable = 14, 224 | IllegalUserName = 15, 225 | } 226 | } 227 | 228 | fn parse_packet_disconnect(i: &[u8]) -> IResult<&[u8], SshPacket<'_>> { 229 | let (i, reason_code) = be_u32(i)?; 230 | let (i, description) = parse_string(i)?; 231 | let (i, lang) = parse_string(i)?; 232 | let packet = SshPacketDisconnect { 233 | reason_code, 234 | description, 235 | lang, 236 | }; 237 | Ok((i, SshPacket::Disconnect(packet))) 238 | } 239 | 240 | impl<'a> SshPacketDisconnect<'a> { 241 | /// Parse Disconnection Description 242 | pub fn get_description(&self) -> Result<&str, str::Utf8Error> { 243 | str::from_utf8(self.description) 244 | } 245 | 246 | /// Parse Disconnection Reason Code 247 | pub fn get_reason(&self) -> SshDisconnectReason { 248 | SshDisconnectReason(self.reason_code) 249 | } 250 | } 251 | 252 | /// SSH Debug Message 253 | /// 254 | /// Defined in [RFC4253 Section 11.3](https://tools.ietf.org/html/rfc4253#section-11.3). 255 | #[derive(Debug, Eq, PartialEq)] 256 | pub struct SshPacketDebug<'a> { 257 | pub always_display: bool, 258 | pub message: &'a [u8], 259 | pub lang: &'a [u8], 260 | } 261 | 262 | fn parse_packet_debug(i: &[u8]) -> IResult<&[u8], SshPacket<'_>> { 263 | let (i, display) = be_u8(i)?; 264 | let (i, message) = parse_string(i)?; 265 | let (i, lang) = parse_string(i)?; 266 | let packet = SshPacketDebug { 267 | always_display: display > 0, 268 | message, 269 | lang, 270 | }; 271 | Ok((i, SshPacket::Debug(packet))) 272 | } 273 | 274 | impl<'a> SshPacketDebug<'a> { 275 | /// Parse Debug Message 276 | pub fn get_message(&self) -> Result<&str, str::Utf8Error> { 277 | str::from_utf8(self.message) 278 | } 279 | } 280 | 281 | /// A SSH message that may belong to the KEX stage. 282 | /// use [`super::SshKEX`] to parse this message. 283 | #[derive(Debug, Eq, PartialEq)] 284 | pub struct MaybeDiffieHellmanKEX<'a>(pub SshPacketUnparsed<'a>); 285 | 286 | /// SSH Packet Enumeration 287 | #[derive(Debug, Eq, PartialEq)] 288 | pub enum SshPacket<'a> { 289 | Disconnect(SshPacketDisconnect<'a>), 290 | Ignore(&'a [u8]), 291 | Unimplemented(u32), 292 | Debug(SshPacketDebug<'a>), 293 | ServiceRequest(&'a [u8]), 294 | ServiceAccept(&'a [u8]), 295 | KeyExchange(SshPacketKeyExchange<'a>), 296 | NewKeys, 297 | DiffieHellmanKEX(MaybeDiffieHellmanKEX<'a>), 298 | } 299 | 300 | /// Parse a plaintext SSH packet with its padding. 301 | /// 302 | /// Packet structure is defined in [RFC4253 Section 6](https://tools.ietf.org/html/rfc4253#section-6) and 303 | /// message codes are defined in [RFC4253 Section 12](https://tools.ietf.org/html/rfc4253#section-12). 304 | pub fn parse_ssh_packet(i: &[u8]) -> IResult<&[u8], (SshPacket<'_>, &[u8])> { 305 | let (i, unparsed_ssh_packet) = parse_ssh_packet_with_message_code(i)?; 306 | let padding = unparsed_ssh_packet.padding; 307 | let d = unparsed_ssh_packet.payload; 308 | let (_, msg) = match unparsed_ssh_packet.message_code { 309 | 1 => parse_packet_disconnect(d), 310 | 2 => map(parse_string, SshPacket::Ignore)(d), 311 | 3 => map(be_u32, SshPacket::Unimplemented)(d), 312 | 4 => parse_packet_debug(d), 313 | 5 => map(parse_string, SshPacket::ServiceRequest)(d), 314 | 6 => map(parse_string, SshPacket::ServiceAccept)(d), 315 | 20 => parse_packet_key_exchange(d), 316 | 21 => Ok((d, SshPacket::NewKeys)), 317 | 30..=34 => Ok(( 318 | i, 319 | SshPacket::DiffieHellmanKEX(MaybeDiffieHellmanKEX(unparsed_ssh_packet)), 320 | )), 321 | _ => Err(Err::Error(make_error(d, ErrorKind::Switch))), 322 | }?; 323 | Ok((i, (msg, padding))) 324 | } 325 | 326 | /// A plaintext SSH packet in raw format, with the message code. 327 | #[derive(Debug, Eq, PartialEq)] 328 | pub struct SshPacketUnparsed<'a> { 329 | /// The payload, **without** the message code byte. 330 | pub payload: &'a [u8], 331 | 332 | /// The padding. 333 | pub padding: &'a [u8], 334 | 335 | /// The message code. 336 | pub message_code: u8, 337 | } 338 | 339 | /// Parse a plaintext SSH packet header with its message code. 340 | /// 341 | /// Packet structure is defined in [RFC4253 Section 6](https://tools.ietf.org/html/rfc4253#section-6) and 342 | pub fn parse_ssh_packet_with_message_code(i: &[u8]) -> IResult<&[u8], SshPacketUnparsed<'_>> { 343 | let (i, packet_length) = be_u32(i)?; 344 | let (i, padding_length) = be_u8(i)?; 345 | if padding_length as u32 + 1 > packet_length { 346 | return Err(Err::Error(make_error(i, ErrorKind::LengthValue))); 347 | } 348 | let (i, payload) = take(packet_length - padding_length as u32 - 1)(i)?; 349 | let (payload_without_message_code, message_code) = be_u8(payload)?; 350 | let (i, padding) = take(padding_length)(i)?; 351 | Ok(( 352 | i, 353 | SshPacketUnparsed { 354 | payload: payload_without_message_code, 355 | padding, 356 | message_code, 357 | }, 358 | )) 359 | } 360 | 361 | #[cfg(test)] 362 | mod tests { 363 | 364 | use super::*; 365 | use nom::Err; 366 | 367 | #[test] 368 | fn test_name() { 369 | let res = parse_name(b"ssh-rsa"); 370 | let expected = Ok((&b""[..], &b"ssh-rsa"[..])); 371 | assert_eq!(res, expected); 372 | } 373 | 374 | #[test] 375 | fn test_empty_name_list() { 376 | let res = parse_name_list(b""); 377 | let expected = Err(Err::Error(make_error(&b""[..], ErrorKind::SeparatedList))); 378 | assert_eq!(res, expected); 379 | } 380 | 381 | #[test] 382 | fn test_one_name_list() { 383 | let res = parse_name_list(b"ssh-rsa"); 384 | let expected = Ok((&b""[..], vec!["ssh-rsa"])); 385 | assert_eq!(res, expected); 386 | } 387 | 388 | #[test] 389 | fn test_two_names_list() { 390 | let res = parse_name_list(b"ssh-rsa,ssh-ecdsa"); 391 | let expected = Ok((&b""[..], vec!["ssh-rsa", "ssh-ecdsa"])); 392 | assert_eq!(res, expected); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/kex.rs: -------------------------------------------------------------------------------- 1 | //! # KEX parser 2 | //! 3 | //! This module contains parsing functions for the Key Exchange part of the 4 | //! SSH 2.0 protocol. 5 | //! 6 | //! The supported Key Exchange protocols are the following: 7 | //! 8 | //! - Diffie Hellman Key Exchange, `SSH_MSG_KEXDH_`, defined in RFC4253 section 8. 9 | //! - Elliptic Curve Diffie Hellman Key Exchange, `SSH_MSG_KEXECDH_INIT`, defined 10 | //! in RFC6239 sections 4.1 and 4.2. 11 | //! - Diffie Hellman Group and Key Exchange, `SSH_MSG_KEY_DH_GEX_`, defined in 12 | //! RFC4419 section 5. 13 | 14 | #[cfg(feature = "integers")] 15 | use std::marker::PhantomData; 16 | 17 | use nom::bytes::complete::take; 18 | use nom::combinator::{all_consuming, map, map_parser, rest}; 19 | use nom::error::Error; 20 | use nom::number::streaming::be_u32; 21 | use nom::sequence::{pair, tuple}; 22 | use nom::IResult; 23 | #[cfg(feature = "integers")] 24 | use num_bigint::BigInt; 25 | 26 | use super::ssh::parse_string; 27 | 28 | use super::{SshPacketKeyExchange, SshPacketUnparsed}; 29 | 30 | /// Diffie-Hellman Key Exchange Init message code. 31 | /// Defined in [RFC4253 errata 1486](https://www.rfc-editor.org/errata/eid1486). 32 | pub const SSH_MSG_KEXDH_INIT: u8 = 30; 33 | 34 | /// Diffie-Hellman Key Exchange Reply message code. 35 | /// Defined in [RFC4253 errata 1486](https://www.rfc-editor.org/errata/eid1486). 36 | pub const SSH_MSG_KEXDH_REPLY: u8 = 31; 37 | 38 | /// Elliptic Curve Diffie-Hellman Key Exchange Init message code. 39 | /// Defined in [RFC6239 section 4.1](https://datatracker.ietf.org/doc/html/rfc6239#section-4.1). 40 | pub const SSH_MSG_KEXECDH_INIT: u8 = SSH_MSG_KEXDH_INIT; 41 | 42 | /// Elliptic Curve Diffie-Hellman Key Exchange Reply message code. 43 | /// Defined in [RFC6239 section 4.2](https://datatracker.ietf.org/doc/html/rfc6239#section-4.2). 44 | pub const SSH_MSG_KEXECDH_REPLY: u8 = SSH_MSG_KEXDH_REPLY; 45 | 46 | /// Diffie-Hellman Group and Key Exchange Request message code. 47 | /// Defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). 48 | pub const SSH_MSG_KEX_DH_GEX_REQUEST: u8 = 34; 49 | 50 | /// Diffie-Hellman Group and Key Exchange Request Old message code. 51 | /// Defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). 52 | pub const SSH_MSG_KEX_DH_GEX_REQUEST_OLD: u8 = 30; 53 | 54 | /// Diffie-Hellman Group and Key Exchange Group message code. 55 | /// Defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). 56 | pub const SSH_MSG_KEX_DH_GEX_GROUP: u8 = 31; 57 | 58 | /// Diffie-Hellman Group and Key Exchange Init message code. 59 | /// Defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). 60 | pub const SSH_MSG_KEX_DH_GEX_INIT: u8 = 32; 61 | 62 | /// Diffie-Hellman Group and Key Exchange Reply message code. 63 | /// Defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). 64 | pub const SSH_MSG_KEX_DH_GEX_REPLY: u8 = 33; 65 | 66 | /// PQ/T Hybrid Key Exchange Init message code. 67 | /// Defined in [draft RFC `draft-kampanakis-curdle-ssh-pq-ke-02` section 2.2](https://www.ietf.org/archive/id/draft-kampanakis-curdle-ssh-pq-ke-02.html#section-2.2) 68 | pub const SSH_MSG_KEX_HYBRID_INIT: u8 = 30; 69 | 70 | /// PQ/T Hybrid Key Exchange Reply message code. 71 | /// Defined in [draft RFC `draft-kampanakis-curdle-ssh-pq-ke-02` section 2.2](https://www.ietf.org/archive/id/draft-kampanakis-curdle-ssh-pq-ke-02.html#section-2.2) 72 | pub const SSH_MSG_KEX_HYBRID_REPLY: u8 = 31; 73 | 74 | /// Supported PQ/T Hybrid Key Exchange algorithm. 75 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 76 | pub enum SupportedHybridKEXAlgorithm { 77 | /// ecdh-nistp256-kyber-512r3-sha256-d00@openquantumsafe.org 78 | ECDHNistP256Kyber512r3Sha256D00OQS, 79 | 80 | /// ecdh-nistp384-kyber-768r3-sha384-d00@openquantumsafe.org 81 | ECDHNistP384Kyber768r3Sha384D00OQS, 82 | 83 | /// ecdh-nistp521-kyber-1024r3-sha512-d00@openquantumsafe.org 84 | ECDHNistP521Kyber1024r3Sha512D00OQS, 85 | } 86 | 87 | impl SupportedHybridKEXAlgorithm { 88 | /// Returns the length in bytes of the post-quantum KEM public key. 89 | pub fn pq_pub_key_len(self) -> usize { 90 | match self { 91 | Self::ECDHNistP256Kyber512r3Sha256D00OQS => 800, 92 | Self::ECDHNistP384Kyber768r3Sha384D00OQS => 1184, 93 | Self::ECDHNistP521Kyber1024r3Sha512D00OQS => 1568, 94 | } 95 | } 96 | 97 | /// Returns the length in bytes of the ciphertext produced by the KEM algorithm. 98 | pub fn pq_ciphertext_len(self) -> usize { 99 | match self { 100 | Self::ECDHNistP256Kyber512r3Sha256D00OQS => 768, 101 | Self::ECDHNistP384Kyber768r3Sha384D00OQS => 1088, 102 | Self::ECDHNistP521Kyber1024r3Sha512D00OQS => 1568, 103 | } 104 | } 105 | } 106 | 107 | #[cfg(feature = "integers")] 108 | fn parse_mpint(i: &[u8]) -> IResult<&[u8], BigInt> { 109 | map_parser(parse_string, crate::mpint::parse_ssh_mpint)(i) 110 | } 111 | 112 | #[cfg(not(feature = "integers"))] 113 | fn parse_mpint(i: &[u8]) -> IResult<&[u8], &[u8]> { 114 | parse_string(i) 115 | } 116 | 117 | /// SSH Diffie-Hellman Key Exchange Init. 118 | /// 119 | /// The message code is `SSH_MSG_KEXDH_INIT`, defined in [RFC4253 section 8](https://datatracker.ietf.org/doc/html/rfc4253#section-8). 120 | #[derive(Debug, PartialEq)] 121 | pub struct SshPacketDHKEXInit<'a> { 122 | /// The public key. 123 | #[cfg(feature = "integers")] 124 | pub e: BigInt, 125 | 126 | /// The public key. 127 | #[cfg(not(feature = "integers"))] 128 | pub e: &'a [u8], 129 | 130 | #[cfg(feature = "integers")] 131 | phantom: std::marker::PhantomData<&'a [u8]>, 132 | } 133 | 134 | #[cfg(feature = "integers")] 135 | impl From for SshPacketDHKEXInit<'_> { 136 | fn from(e: BigInt) -> Self { 137 | Self { 138 | e, 139 | phantom: PhantomData, 140 | } 141 | } 142 | } 143 | 144 | #[cfg(not(feature = "integers"))] 145 | impl<'a> From<&'a [u8]> for SshPacketDHKEXInit<'a> { 146 | fn from(e: &'a [u8]) -> Self { 147 | Self { e } 148 | } 149 | } 150 | 151 | impl<'a> SshPacketDHKEXInit<'a> { 152 | /// Parses a SSH Diffie-Hellman Key Exchange Init. 153 | pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { 154 | map(parse_mpint, Self::from)(i) 155 | } 156 | } 157 | 158 | /// SSH Diffie-Hellman Key Exchange Reply. 159 | /// 160 | /// The message code is `SSH_MSG_KEXDH_REPLY`, defined in [RFC4253 section 8](https://datatracker.ietf.org/doc/html/rfc4253#section-8). 161 | #[derive(Debug, PartialEq)] 162 | pub struct SshPacketDHKEXReply<'a> { 163 | /// The server public host key and certificate. 164 | pub pubkey_and_cert: &'a [u8], 165 | 166 | /// The `f` value corresponding to `g^y mod p` where `g` is the group and `y` a random number. 167 | #[cfg(feature = "integers")] 168 | pub f: BigInt, 169 | 170 | /// The `f` value corresponding to `g^y mod p` where `g` is the group and `y` a random number. 171 | #[cfg(not(feature = "integers"))] 172 | pub f: &'a [u8], 173 | 174 | /// The signature. 175 | pub signature: &'a [u8], 176 | } 177 | 178 | #[cfg(feature = "integers")] 179 | impl<'a, 'b, 'c> From<(&'b [u8], BigInt, &'c [u8])> for SshPacketDHKEXReply<'a> 180 | where 181 | 'b: 'a, 182 | 'c: 'a, 183 | { 184 | fn from((pubkey_and_cert, f, signature): (&'b [u8], BigInt, &'c [u8])) -> Self { 185 | Self { 186 | pubkey_and_cert, 187 | f, 188 | signature, 189 | } 190 | } 191 | } 192 | 193 | #[cfg(not(feature = "integers"))] 194 | impl<'a, 'b, 'c, 'd> From<(&'b [u8], &'c [u8], &'d [u8])> for SshPacketDHKEXReply<'a> 195 | where 196 | 'b: 'a, 197 | 'c: 'a, 198 | 'd: 'a, 199 | { 200 | fn from((pubkey_and_cert, f, signature): (&'b [u8], &'c [u8], &'d [u8])) -> Self { 201 | Self { 202 | pubkey_and_cert, 203 | f, 204 | signature, 205 | } 206 | } 207 | } 208 | 209 | /// An ECDSA signature. 210 | /// 211 | /// ECDSA signatures are defined in [RFC5656 Section 3.1.2](https://tools.ietf.org/html/rfc5656#section-3.1.2). 212 | #[derive(Debug, PartialEq)] 213 | pub struct ECDSASignature<'a> { 214 | /// Identifier. 215 | pub identifier: &'a str, 216 | 217 | /// Blob. 218 | pub blob: &'a [u8], 219 | } 220 | 221 | impl<'a> SshPacketDHKEXReply<'a> { 222 | pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { 223 | map(tuple((parse_string, parse_mpint, parse_string)), Self::from)(i) 224 | } 225 | 226 | /// Parses the ECDSA signature. 227 | /// 228 | /// ECDSA signatures are Defined in [RFC5656 Section 3.1.2](https://tools.ietf.org/html/rfc5656#section-3.1.2). 229 | pub fn get_ecdsa_signature(&self) -> Result, SshKEXError<'a>> { 230 | let (_, (identifier, blob)) = pair(parse_string, parse_string)(self.signature)?; 231 | 232 | let identifier = std::str::from_utf8(identifier)?; 233 | Ok(ECDSASignature { identifier, blob }) 234 | } 235 | } 236 | 237 | /// The key exchange protocol using Diffie Hellman Key Exchange, defined in RFC4253. 238 | #[derive(Debug, Default, PartialEq)] 239 | pub struct SshKEXDiffieHellman<'a> { 240 | /// The init message, i.e. `SSH_MSG_KEXDH_INIT`. 241 | pub init: Option>, 242 | 243 | /// The reply message, i.e. `SSH_MSG_KEXDH_REPLY`. 244 | pub reply: Option>, 245 | } 246 | 247 | /// SSH Elliptic Curve Diffie-Hellman Key Exchange Init. 248 | /// 249 | /// The message is `SSH_MSG_KEXECDH_INIT`, defined in [RFC6239 section 4.1](https://datatracker.ietf.org/doc/html/rfc6239#section-4.1). 250 | #[derive(Debug, PartialEq)] 251 | pub struct SshPacketECDHKEXInit<'a> { 252 | /// The client's ephemeral contribution to theECDH exchange, encoded as an octet string. 253 | pub q_c: &'a [u8], 254 | } 255 | 256 | impl<'a, 'b> From<&'b [u8]> for SshPacketECDHKEXInit<'a> 257 | where 258 | 'b: 'a, 259 | { 260 | fn from(q_c: &'b [u8]) -> Self { 261 | Self { q_c } 262 | } 263 | } 264 | 265 | impl<'a> SshPacketECDHKEXInit<'a> { 266 | pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { 267 | map(parse_string, Self::from)(i) 268 | } 269 | } 270 | 271 | /// SSH Elliptic Curve Diffie-Hellman Key Exchange Reply. 272 | /// 273 | /// The message is `SSH_MSG_KEXECDH_REPLY`, defined in [RFC6239 section 4.2](https://datatracker.ietf.org/doc/html/rfc6239#section-4.2). 274 | #[derive(Debug, PartialEq)] 275 | pub struct SshPacketECDHKEXReply<'a> { 276 | /// A string encoding an X.509v3 certificate containing the server's ECDSA public host key. 277 | pub pubkey_and_cert: &'a [u8], 278 | 279 | /// The server's ephemeral contribution to the ECDH exchange, encoded as an octet string. 280 | pub q_s: &'a [u8], 281 | 282 | /// The server's signature of the newly established exchange hash value. 283 | pub signature: &'a [u8], 284 | } 285 | 286 | impl<'a, 'b, 'c, 'd> From<(&'b [u8], &'c [u8], &'d [u8])> for SshPacketECDHKEXReply<'a> 287 | where 288 | 'b: 'a, 289 | 'c: 'a, 290 | 'd: 'a, 291 | { 292 | fn from((pubkey_and_cert, q_s, signature): (&'b [u8], &'c [u8], &'d [u8])) -> Self { 293 | Self { 294 | pubkey_and_cert, 295 | q_s, 296 | signature, 297 | } 298 | } 299 | } 300 | 301 | impl<'a> SshPacketECDHKEXReply<'a> { 302 | pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { 303 | map( 304 | tuple((parse_string, parse_string, parse_string)), 305 | Self::from, 306 | )(i) 307 | } 308 | } 309 | 310 | /// The key exchange protocol using Elliptic Curve Diffie Hellman Key Exchange, defined in RFC6239. 311 | #[derive(Debug, Default, PartialEq)] 312 | pub struct SshKEXECDiffieHellman<'a> { 313 | /// The init message, i.e. `SSH_MSG_KEXECDH_INIT`. 314 | pub init: Option>, 315 | 316 | /// The reply message, i.e. `SSH_MSG_KEXECDH_REPLY`. 317 | pub reply: Option>, 318 | } 319 | 320 | /// SSH Diffie-Hellman Group and Key Exchange Request. 321 | /// 322 | /// The message code is `SSH_MSG_KEY_DH_GEX_REQUEST`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). 323 | /// 324 | /// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). 325 | #[derive(Debug, PartialEq)] 326 | pub struct SshPacketDhKEXGEXRequest { 327 | /// Minimal size in bits of an acceptable group. 328 | pub min: u32, 329 | 330 | /// Preferred size in bits of the group the server will send. 331 | pub n: u32, 332 | 333 | /// Maximal size in bits of an acceptable group. 334 | pub max: u32, 335 | } 336 | 337 | impl From<(u32, u32, u32)> for SshPacketDhKEXGEXRequest { 338 | fn from((min, n, max): (u32, u32, u32)) -> Self { 339 | Self { min, n, max } 340 | } 341 | } 342 | 343 | impl SshPacketDhKEXGEXRequest { 344 | pub fn parse(i: &[u8]) -> IResult<&[u8], Self> { 345 | map(tuple((be_u32, be_u32, be_u32)), Self::from)(i) 346 | } 347 | } 348 | 349 | /// SSH Diffie-Hellman Group and Key Exchange Request (old). 350 | /// 351 | /// The message code is `SSH_MSG_KEY_DH_GEX_REQUEST_OLD`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). 352 | /// 353 | /// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). 354 | #[derive(Debug, PartialEq)] 355 | pub struct SshPacketDhKEXGEXRequestOld { 356 | /// Preferred size in bits of the group the server will send. 357 | pub n: u32, 358 | } 359 | 360 | impl From for SshPacketDhKEXGEXRequestOld { 361 | fn from(n: u32) -> Self { 362 | Self { n } 363 | } 364 | } 365 | 366 | impl SshPacketDhKEXGEXRequestOld { 367 | pub fn parse(i: &[u8]) -> IResult<&[u8], Self> { 368 | map(be_u32, Self::from)(i) 369 | } 370 | } 371 | 372 | /// SSH Diffie-Hellman Group and Key Exchange Group. 373 | /// 374 | /// The message code is `SSH_MSG_KEX_DH_GEX_GROUP`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). 375 | /// 376 | /// 377 | /// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). 378 | #[derive(Debug, PartialEq)] 379 | pub struct SshPacketDhKEXGEXGroup<'a> { 380 | /// The safe prime. 381 | #[cfg(feature = "integers")] 382 | pub p: BigInt, 383 | 384 | /// The safe prime. 385 | #[cfg(not(feature = "integers"))] 386 | pub p: &'a [u8], 387 | 388 | /// The generator for the subgroup in the Galois Field GF(p). 389 | #[cfg(feature = "integers")] 390 | pub g: BigInt, 391 | 392 | /// The generator for the subgroup in the Galois Field GF(p). 393 | #[cfg(not(feature = "integers"))] 394 | pub g: &'a [u8], 395 | 396 | #[cfg(feature = "integers")] 397 | phantom: PhantomData<&'a [u8]>, 398 | } 399 | 400 | #[cfg(feature = "integers")] 401 | impl From<(BigInt, BigInt)> for SshPacketDhKEXGEXGroup<'_> { 402 | fn from((p, g): (BigInt, BigInt)) -> Self { 403 | Self { 404 | p, 405 | g, 406 | phantom: PhantomData, 407 | } 408 | } 409 | } 410 | 411 | #[cfg(not(feature = "integers"))] 412 | impl<'a, 'b, 'c> From<(&'b [u8], &'c [u8])> for SshPacketDhKEXGEXGroup<'a> 413 | where 414 | 'b: 'a, 415 | 'c: 'a, 416 | { 417 | fn from((p, g): (&'b [u8], &'c [u8])) -> Self { 418 | Self { p, g } 419 | } 420 | } 421 | 422 | impl<'a> SshPacketDhKEXGEXGroup<'a> { 423 | pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { 424 | map(pair(parse_mpint, parse_mpint), Self::from)(i) 425 | } 426 | } 427 | 428 | /// SSH Diffie-Hellman Group and Key Exchange Init. 429 | /// 430 | /// The message code is `SSH_MSG_KEX_DH_GEX_INIT`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). 431 | /// 432 | /// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). 433 | #[derive(Debug, PartialEq)] 434 | pub struct SshPacketDhKEXGEXInit<'a> { 435 | /// The public key. 436 | #[cfg(feature = "integers")] 437 | pub e: BigInt, 438 | 439 | /// The public key. 440 | #[cfg(not(feature = "integers"))] 441 | pub e: &'a [u8], 442 | 443 | #[cfg(feature = "integers")] 444 | phantom: PhantomData<&'a [u8]>, 445 | } 446 | 447 | #[cfg(feature = "integers")] 448 | impl From for SshPacketDhKEXGEXInit<'_> { 449 | fn from(e: BigInt) -> Self { 450 | Self { 451 | e, 452 | phantom: PhantomData, 453 | } 454 | } 455 | } 456 | 457 | #[cfg(not(feature = "integers"))] 458 | impl<'a, 'b> From<&'b [u8]> for SshPacketDhKEXGEXInit<'a> 459 | where 460 | 'b: 'a, 461 | { 462 | fn from(e: &'b [u8]) -> Self { 463 | Self { e } 464 | } 465 | } 466 | 467 | /// Parses a SSH Diffie-Hellman Group and Key Exchange init. 468 | impl<'a> SshPacketDhKEXGEXInit<'a> { 469 | pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { 470 | map(parse_mpint, Self::from)(i) 471 | } 472 | } 473 | 474 | /// SSH Diffie-Hellman Group and Key Exchange Reply. 475 | /// 476 | /// The message code is `SSH_MSG_KEX_DH_GEX_REPLY`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). 477 | /// 478 | /// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). 479 | #[derive(Debug, PartialEq)] 480 | pub struct SshPacketDhKEXGEXReply<'a> { 481 | /// Server public host key and certificate. 482 | pub pubkey_and_cert: &'a [u8], 483 | 484 | /// f. 485 | #[cfg(feature = "integers")] 486 | pub f: BigInt, 487 | 488 | /// f. 489 | #[cfg(not(feature = "integers"))] 490 | pub f: &'a [u8], 491 | 492 | /// Signature. 493 | pub signature: &'a [u8], 494 | } 495 | 496 | #[cfg(feature = "integers")] 497 | impl<'a, 'b, 'c> From<(&'b [u8], BigInt, &'c [u8])> for SshPacketDhKEXGEXReply<'a> 498 | where 499 | 'b: 'a, 500 | 'c: 'a, 501 | { 502 | fn from((pubkey_and_cert, f, signature): (&'b [u8], BigInt, &'c [u8])) -> Self { 503 | Self { 504 | pubkey_and_cert, 505 | f, 506 | signature, 507 | } 508 | } 509 | } 510 | 511 | #[cfg(not(feature = "integers"))] 512 | impl<'a, 'b, 'c, 'd> From<(&'b [u8], &'c [u8], &'d [u8])> for SshPacketDhKEXGEXReply<'a> 513 | where 514 | 'b: 'a, 515 | 'c: 'a, 516 | 'd: 'a, 517 | { 518 | fn from((pubkey_and_cert, f, signature): (&'b [u8], &'c [u8], &'d [u8])) -> Self { 519 | Self { 520 | pubkey_and_cert, 521 | f, 522 | signature, 523 | } 524 | } 525 | } 526 | 527 | impl<'a> SshPacketDhKEXGEXReply<'a> { 528 | pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { 529 | map(tuple((parse_string, parse_mpint, parse_string)), Self::from)(i) 530 | } 531 | } 532 | 533 | /// The key exchange protocol using Diffie Hellman Group and Key, defined in RFC4419. 534 | #[derive(Debug, Default, PartialEq)] 535 | pub struct SshKEXDiffieHellmanKEXGEX<'a> { 536 | /// The request message, i.e. `SSH_MSG_KEY_DH_GEX_REQUEST`. 537 | pub request: Option, 538 | 539 | /// The request message (old variant), i.e. `SSH_MSG_KEY_DH_GEX_REQUEST_OLD`. 540 | pub request_old: Option, 541 | 542 | /// The group message, i.e. `SSH_MSG_KEX_DH_GEX_GROUP`. 543 | pub group: Option>, 544 | 545 | /// The init message, i.e. `SSH_MSG_KEX_DH_GEX_INIT`. 546 | pub init: Option>, 547 | 548 | /// The init message, i.e. `SSH_MSG_KEX_DH_GEX_REPLY`. 549 | pub reply: Option>, 550 | } 551 | 552 | /// SSH Hybrid Key Exchange init. 553 | /// 554 | /// The message code is `SSH_MSG_KEX_HYBRID_INIT`, defined in 555 | /// [draft RFC `draft-kampanakis-curdle-ssh-pq-ke-02` section 2.2](https://www.ietf.org/archive/id/draft-kampanakis-curdle-ssh-pq-ke-02.html#section-2.2) 556 | #[derive(Debug, PartialEq)] 557 | pub struct SshPacketHybridKEXInit<'a> { 558 | /// The post-quantum KEM's public key (`C_PK2`). 559 | pub pq_pub_key: &'a [u8], 560 | 561 | /// The traditional / classical KEX public key. 562 | pub classical_pub_key: &'a [u8], 563 | } 564 | 565 | impl<'a> SshPacketHybridKEXInit<'a> { 566 | /// Parses a SSH PQ/T Hybrid Key Exchange Init. 567 | pub fn parse(i: &'a [u8], alg: SupportedHybridKEXAlgorithm) -> IResult<&'a [u8], Self> { 568 | let pq_len = alg.pq_pub_key_len(); 569 | let (i, (pq_pub_key, classical_pub_key)) = 570 | map_parser(parse_string, tuple((take(pq_len), rest)))(i)?; 571 | Ok(( 572 | i, 573 | Self { 574 | pq_pub_key, 575 | classical_pub_key, 576 | }, 577 | )) 578 | } 579 | } 580 | 581 | /// SSH Hybrid Key Exchange reply. 582 | /// 583 | /// The message code is `SSH_MSG_KEX_HYBRID_REPLY`, defined in 584 | /// [draft RFC `draft-kampanakis-curdle-ssh-pq-ke-02` section 2.2](https://www.ietf.org/archive/id/draft-kampanakis-curdle-ssh-pq-ke-02.html#section-2.2) 585 | #[derive(Debug, PartialEq)] 586 | pub struct SshPacketHybridKEXReply<'a> { 587 | /// K_S, server's public host key. 588 | pub pubkey_and_cert: &'a [u8], 589 | 590 | /// S_CT2, the ciphertext 'ct' output of the corresponding KEM's 'Encaps' algorithm. 591 | pub pq_ciphertext: &'a [u8], 592 | 593 | /// S_PK1, ephemeral (EC)DH server public key. 594 | pub classical_pub_key: &'a [u8], 595 | 596 | /// Signature. 597 | pub signature: &'a [u8], 598 | } 599 | 600 | impl<'a> SshPacketHybridKEXReply<'a> { 601 | /// Parses a SSH PQ/T Hybrid Key Exchange reply. 602 | pub fn parse(i: &'a [u8], alg: SupportedHybridKEXAlgorithm) -> IResult<&'a [u8], Self> { 603 | let ct_len = alg.pq_ciphertext_len(); 604 | let (i, (pubkey_and_cert, (pq_ciphertext, classical_pub_key), signature)) = tuple(( 605 | parse_string, 606 | map_parser(parse_string, tuple((take(ct_len), rest))), 607 | parse_string, 608 | ))(i)?; 609 | Ok(( 610 | i, 611 | Self { 612 | pubkey_and_cert, 613 | pq_ciphertext, 614 | classical_pub_key, 615 | signature, 616 | }, 617 | )) 618 | } 619 | } 620 | 621 | /// The key exchange protocol using PQ/T Key Exchange, defined in 622 | /// [draft RFC `draft-kampanakis-curdle-ssh-pq-ke-02`](https://www.ietf.org/archive/id/draft-kampanakis-curdle-ssh-pq-ke-02.html). 623 | #[derive(Debug, PartialEq)] 624 | pub struct SshHybridKEX<'a> { 625 | /// The init message, i.e. `SSH_MSG_KEX_HYBRID_INIT`. 626 | pub init: Option>, 627 | 628 | /// The reply message, i.e. `SSH_MSG_KEX_HYBRID_REPLY`. 629 | pub reply: Option>, 630 | 631 | /// The algorithm. 632 | pub alg: SupportedHybridKEXAlgorithm, 633 | } 634 | 635 | impl SshHybridKEX<'_> { 636 | /// Initializes a new [`SshHybridKEX`] using the given algorithm. 637 | pub fn new(alg: SupportedHybridKEXAlgorithm) -> Self { 638 | Self { 639 | init: None, 640 | reply: None, 641 | alg, 642 | } 643 | } 644 | } 645 | 646 | /// An error occurring in the KEX parser. 647 | #[derive(Debug)] 648 | pub enum SshKEXError<'a> { 649 | /// nom error. 650 | Nom(nom::Err>), 651 | 652 | /// Could not negociate a KEX algorithm. 653 | NegociationFailed, 654 | 655 | /// Unknown KEX protocol. 656 | UnknownProtocol, 657 | 658 | /// Duplicated message. 659 | DuplicatedMessage, 660 | 661 | /// Unexpected message. 662 | UnexpectedMessage, 663 | 664 | /// Invalid UTF-8 string. 665 | InvalidUtf8(std::str::Utf8Error), 666 | 667 | /// Other error. 668 | Other(String), 669 | } 670 | 671 | impl std::fmt::Display for SshKEXError<'_> { 672 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 673 | write!(f, "{self:?}") 674 | } 675 | } 676 | 677 | impl<'a> From>> for SshKEXError<'a> { 678 | fn from(e: nom::Err>) -> Self { 679 | Self::Nom(e) 680 | } 681 | } 682 | 683 | impl From for SshKEXError<'_> { 684 | fn from(e: String) -> Self { 685 | Self::Other(e) 686 | } 687 | } 688 | 689 | impl From<&str> for SshKEXError<'_> { 690 | fn from(e: &str) -> Self { 691 | Self::Other(e.to_string()) 692 | } 693 | } 694 | 695 | impl From for SshKEXError<'_> { 696 | fn from(e: std::str::Utf8Error) -> Self { 697 | Self::InvalidUtf8(e) 698 | } 699 | } 700 | 701 | impl std::error::Error for SshKEXError<'_> {} 702 | 703 | macro_rules! parse_match_and_assign { 704 | ($variant:ident, $field:ident, $struct:ident, $payload:ident) => { 705 | if $variant.$field.is_some() { 706 | Err(SshKEXError::DuplicatedMessage) 707 | } else { 708 | $variant.$field = Some(all_consuming($struct::parse)($payload)?.1); 709 | Ok(()) 710 | } 711 | }; 712 | } 713 | 714 | /// Parses a hybrid KEX message, matches its owner and assign the parsed 715 | /// object to it. 716 | /// 717 | /// We use a macro here because we take a field of `SshHybridKEX` as a parameter 718 | /// (the receiver). 719 | macro_rules! parse_match_and_assign_hybrid { 720 | ($variant:ident, $field:ident, $struct:ident, $payload:ident) => { 721 | if $variant.$field.is_some() { 722 | Err(SshKEXError::DuplicatedMessage) 723 | } else { 724 | let alg = $variant.alg; 725 | $variant.$field = Some(all_consuming(|i| $struct::parse(i, alg))($payload)?.1); 726 | Ok(()) 727 | } 728 | }; 729 | } 730 | 731 | /// Negociates the KEX algorithm. 732 | pub fn ssh_kex_negociate_algorithm<'a, 'b, 'c, S1, S2>( 733 | client_kex_algs: impl IntoIterator, 734 | server_kex_algs: impl IntoIterator, 735 | ) -> Option<&'a str> 736 | where 737 | 'b: 'a, 738 | 'c: 'a, 739 | S1: AsRef + 'b + ?Sized, 740 | S2: AsRef + 'c + ?Sized, 741 | { 742 | let server_algs = server_kex_algs 743 | .into_iter() 744 | .map(|s| s.as_ref()) 745 | .collect::>(); 746 | client_kex_algs 747 | .into_iter() 748 | .find(|&item| server_algs.contains(&item.as_ref())) 749 | .map(|s| s.as_ref()) 750 | } 751 | 752 | /// The key exchange protocol. 753 | #[derive(Debug, PartialEq)] 754 | pub enum SshKEX<'a> { 755 | /// Diffie Hellman Key Exchange, defined in RFC4253. 756 | DiffieHellman(SshKEXDiffieHellman<'a>), 757 | 758 | /// Elliptic Curve Diffie Hellman, defined in RFC6239. 759 | ECDiffieHellman(SshKEXECDiffieHellman<'a>), 760 | 761 | /// Diffie Hellman Group and Key, defined in RFC4419. 762 | DiffieHellmanKEXGEX(SshKEXDiffieHellmanKEXGEX<'a>), 763 | 764 | /// PQ/T Hybrid Key Exchange, defined in 765 | /// [draft RFC `draft-kampanakis-curdle-ssh-pq-ke-02`](https://www.ietf.org/archive/id/draft-kampanakis-curdle-ssh-pq-ke-02.html). 766 | HybridKEX(SshHybridKEX<'a>), 767 | } 768 | 769 | impl<'a> SshKEX<'a> { 770 | /// Initializes a [`SshKEX`] using the kex algorithms sent during the kex exchange 771 | /// init stage. 772 | /// The returned string is the negociated KEX algorithm. 773 | pub fn init<'b, 'c>( 774 | client_kex_init: &'b SshPacketKeyExchange<'b>, 775 | server_kex_init: &'c SshPacketKeyExchange<'c>, 776 | ) -> Result<(Self, &'a str), SshKEXError<'a>> 777 | where 778 | 'b: 'a, 779 | 'c: 'a, 780 | { 781 | let client_kex_list = client_kex_init.get_kex_algs()?; 782 | let server_kex_list = server_kex_init.get_kex_algs()?; 783 | let negociated_alg = ssh_kex_negociate_algorithm(client_kex_list, server_kex_list) 784 | .ok_or(SshKEXError::NegociationFailed)?; 785 | match negociated_alg { 786 | "diffie-hellman-group1-sha1" 787 | | "diffie-hellman-group14-sha1" 788 | | "diffie-hellman-group14-sha256" 789 | | "diffie-hellman-group16-sha512" 790 | | "diffie-hellman-group18-sha512" => { 791 | Ok(Self::DiffieHellman(SshKEXDiffieHellman::default())) 792 | } 793 | "curve25519-sha256" 794 | | "curve25519-sha256@libssh.org" 795 | | "curve448-sha512" 796 | | "ecdh-sha2-nistp256" 797 | | "ecdh-sha2-nistp384" 798 | | "ecdh-sha2-nistp521" => Ok(Self::ECDiffieHellman(SshKEXECDiffieHellman::default())), 799 | "diffie-hellman-group-exchange-sha1" | "diffie-hellman-group-exchange-sha256" => Ok( 800 | Self::DiffieHellmanKEXGEX(SshKEXDiffieHellmanKEXGEX::default()), 801 | ), 802 | "ecdh-nistp256-kyber-512r3-sha256-d00@openquantumsafe.org" => Ok(Self::HybridKEX( 803 | SshHybridKEX::new(SupportedHybridKEXAlgorithm::ECDHNistP256Kyber512r3Sha256D00OQS), 804 | )), 805 | "ecdh-nistp384-kyber-768r3-sha384-d00@openquantumsafe.org" => Ok(Self::HybridKEX( 806 | SshHybridKEX::new(SupportedHybridKEXAlgorithm::ECDHNistP384Kyber768r3Sha384D00OQS), 807 | )), 808 | "ecdh-nistp521-kyber-1024r3-sha512-d00@openquantumsafe.org" => Ok(Self::HybridKEX( 809 | SshHybridKEX::new(SupportedHybridKEXAlgorithm::ECDHNistP521Kyber1024r3Sha512D00OQS), 810 | )), 811 | _ => Err(SshKEXError::UnknownProtocol), 812 | } 813 | .map(|kex| (kex, negociated_alg)) 814 | } 815 | 816 | /// Parses a new message according to the selected KEX method. 817 | /// If the parsed message is not related to the KEX protocol, SshKEXError::UnexpectedMessage 818 | /// is returned. 819 | pub fn parse_ssh_packet<'c>( 820 | &mut self, 821 | unparsed_ssh_packet: &'c SshPacketUnparsed<'c>, 822 | ) -> Result<(), SshKEXError<'a>> 823 | where 824 | 'c: 'a, 825 | { 826 | let payload = unparsed_ssh_packet.payload; 827 | match self { 828 | Self::DiffieHellman(dh) => match unparsed_ssh_packet.message_code { 829 | SSH_MSG_KEXDH_INIT => { 830 | parse_match_and_assign!(dh, init, SshPacketDHKEXInit, payload) 831 | } 832 | SSH_MSG_KEXDH_REPLY => { 833 | parse_match_and_assign!(dh, reply, SshPacketDHKEXReply, payload) 834 | } 835 | _ => Err(SshKEXError::UnexpectedMessage), 836 | }, 837 | Self::ECDiffieHellman(dh) => match unparsed_ssh_packet.message_code { 838 | SSH_MSG_KEXECDH_INIT => { 839 | parse_match_and_assign!(dh, init, SshPacketECDHKEXInit, payload) 840 | } 841 | SSH_MSG_KEXECDH_REPLY => { 842 | parse_match_and_assign!(dh, reply, SshPacketECDHKEXReply, payload) 843 | } 844 | _ => Err(SshKEXError::UnexpectedMessage), 845 | }, 846 | Self::DiffieHellmanKEXGEX(dh) => match unparsed_ssh_packet.message_code { 847 | SSH_MSG_KEX_DH_GEX_REQUEST => { 848 | parse_match_and_assign!(dh, request, SshPacketDhKEXGEXRequest, payload) 849 | } 850 | SSH_MSG_KEX_DH_GEX_REQUEST_OLD => { 851 | parse_match_and_assign!(dh, request_old, SshPacketDhKEXGEXRequestOld, payload) 852 | } 853 | SSH_MSG_KEX_DH_GEX_GROUP => { 854 | parse_match_and_assign!(dh, group, SshPacketDhKEXGEXGroup, payload) 855 | } 856 | SSH_MSG_KEX_DH_GEX_INIT => { 857 | parse_match_and_assign!(dh, init, SshPacketDhKEXGEXInit, payload) 858 | } 859 | SSH_MSG_KEX_DH_GEX_REPLY => { 860 | parse_match_and_assign!(dh, reply, SshPacketDhKEXGEXReply, payload) 861 | } 862 | _ => Err(SshKEXError::UnexpectedMessage), 863 | }, 864 | Self::HybridKEX(hk) => match unparsed_ssh_packet.message_code { 865 | SSH_MSG_KEX_HYBRID_INIT => { 866 | parse_match_and_assign_hybrid!(hk, init, SshPacketHybridKEXInit, payload) 867 | } 868 | SSH_MSG_KEX_HYBRID_REPLY => { 869 | parse_match_and_assign_hybrid!(hk, reply, SshPacketHybridKEXReply, payload) 870 | } 871 | _ => Err(SshKEXError::UnexpectedMessage), 872 | }, 873 | } 874 | } 875 | } 876 | --------------------------------------------------------------------------------