diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..92698890 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.git +.DS_Store +.pijul diff --git a/.ignore b/.ignore new file mode 100644 index 00000000..0863ea9c --- /dev/null +++ b/.ignore @@ -0,0 +1,2 @@ +.git +.DS_Store diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..e3c90d6e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +members = [ "thrussh-keys", "thrussh", "thrussh-libsodium", "thrussh-config", "cryptovec" ] + +[patch.crates-io] +thrussh = { path = "thrussh" } +thrussh-keys = { path = "thrussh-keys" } +thrussh-libsodium = { path = "thrussh-libsodium" } +cryptovec = { path = "cryptovec" } +thrussh-config = { path = "thrussh-config" } \ No newline at end of file diff --git a/LICENSE-2.0.txt b/LICENSE-2.0.txt new file mode 100755 index 00000000..d6456956 --- /dev/null +++ b/LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..fbc525ec --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Thrussh + +A full implementation of the SSH 2 protocol, both server-side and client-side. + +Thrussh is completely asynchronous, and can be combined with other protocols using [Tokio](//tokio.rs). + +## Contributing + +We welcome contributions. Currently, the main areas where we need help are: + +- Handling SSH keys correctly on all platforms. In particular, interactions with agents, PGP, and password-protected/encrypted keys are not yet implemented. + +- Auditing our code, and writing tests. The code is written so that the protocol can be entirely run inside vectors (instead of network sockets). + +By contributing, you agree to license all your contributions under the Apache 2.0 license. + +Moreover, the main platform for contributing is [the Nest](//nest.pijul.com/pijul/thrussh), which is still at an experimental stage. Therefore, even though we do our best to avoid it, our repository might be reset, causing the patches of all contributors to be merged. + + +## Issue Reporting + +Please report bugs on the [issue page of this repository](//nest.pijul.com/pijul/thrussh). +Thrussh has a full disclosure vulnerability policy. +Please do NOT attempt to report any security vulnerability in this code privately to anybody. diff --git a/cryptovec/Cargo.toml b/cryptovec/Cargo.toml new file mode 100644 index 00000000..5ea17c35 --- /dev/null +++ b/cryptovec/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cryptovec" +description = "A vector which zeroes its memory on clears and reallocations." +version = "0.6.1" +authors = ["Pierre-Étienne Meunier "] +repository = "https://pijul.org/cryptovec" +documentation = "https://pijul.org/cryptovec/doc/cryptovec" +license = "Apache-2.0" +include = ["Cargo.toml", "src/lib.rs"] + +[dependencies] +libc = "0.2" +winapi = { version = "0.3", features = ["basetsd", "minwindef", "memoryapi"] } diff --git a/cryptovec/src/lib.rs b/cryptovec/src/lib.rs new file mode 100644 index 00000000..b29b2d43 --- /dev/null +++ b/cryptovec/src/lib.rs @@ -0,0 +1,428 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +extern crate libc; +extern crate winapi; +use libc::c_void; +#[cfg(not(windows))] +use libc::size_t; +use std::ops::{Deref, DerefMut, Index, IndexMut, Range, RangeFrom, RangeFull, RangeTo}; + +/// A buffer which zeroes its memory on `.clear()`, `.resize()` and +/// reallocations, to avoid copying secrets around. +#[derive(Debug)] +pub struct CryptoVec { + p: *mut u8, + size: usize, + capacity: usize, +} + +impl Unpin for CryptoVec {} + +unsafe impl Send for CryptoVec {} +unsafe impl Sync for CryptoVec {} + +impl AsRef<[u8]> for CryptoVec { + fn as_ref(&self) -> &[u8] { + self.deref() + } +} +impl AsMut<[u8]> for CryptoVec { + fn as_mut(&mut self) -> &mut [u8] { + self.deref_mut() + } +} +impl Deref for CryptoVec { + type Target = [u8]; + fn deref(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self.p, self.size) } + } +} +impl DerefMut for CryptoVec { + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { std::slice::from_raw_parts_mut(self.p, self.size) } + } +} + +impl From for CryptoVec { + fn from(e: String) -> Self { + CryptoVec::from(e.into_bytes()) + } +} + +impl From> for CryptoVec { + fn from(e: Vec) -> Self { + let mut c = CryptoVec::new_zeroed(e.len()); + c.clone_from_slice(&e[..]); + c + } +} + +impl Index> for CryptoVec { + type Output = [u8]; + fn index(&self, index: RangeFrom) -> &[u8] { + self.deref().index(index) + } +} +impl Index> for CryptoVec { + type Output = [u8]; + fn index(&self, index: RangeTo) -> &[u8] { + self.deref().index(index) + } +} +impl Index> for CryptoVec { + type Output = [u8]; + fn index(&self, index: Range) -> &[u8] { + self.deref().index(index) + } +} +impl Index for CryptoVec { + type Output = [u8]; + fn index(&self, _: RangeFull) -> &[u8] { + self.deref() + } +} +impl IndexMut for CryptoVec { + fn index_mut(&mut self, _: RangeFull) -> &mut [u8] { + self.deref_mut() + } +} + +impl IndexMut> for CryptoVec { + fn index_mut(&mut self, index: RangeFrom) -> &mut [u8] { + self.deref_mut().index_mut(index) + } +} +impl IndexMut> for CryptoVec { + fn index_mut(&mut self, index: RangeTo) -> &mut [u8] { + self.deref_mut().index_mut(index) + } +} +impl IndexMut> for CryptoVec { + fn index_mut(&mut self, index: Range) -> &mut [u8] { + self.deref_mut().index_mut(index) + } +} + +impl Index for CryptoVec { + type Output = u8; + fn index(&self, index: usize) -> &u8 { + self.deref().index(index) + } +} + +impl std::io::Write for CryptoVec { + fn write(&mut self, buf: &[u8]) -> Result { + self.extend(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> Result<(), std::io::Error> { + Ok(()) + } +} + +impl Default for CryptoVec { + fn default() -> Self { + CryptoVec { + p: std::ptr::NonNull::dangling().as_ptr(), + size: 0, + capacity: 0, + } + } +} + +#[cfg(not(windows))] +unsafe fn mlock(ptr: *const u8, len: usize) { + libc::mlock(ptr as *const c_void, len as size_t); +} +#[cfg(not(windows))] +unsafe fn munlock(ptr: *const u8, len: usize) { + libc::munlock(ptr as *const c_void, len as size_t); +} + +#[cfg(windows)] +use winapi::shared::basetsd::SIZE_T; +#[cfg(windows)] +use winapi::shared::minwindef::LPVOID; +#[cfg(windows)] +use winapi::um::memoryapi::{VirtualLock, VirtualUnlock}; +#[cfg(windows)] +unsafe fn mlock(ptr: *const u8, len: usize) { + VirtualLock(ptr as LPVOID, len as SIZE_T); +} +#[cfg(windows)] +unsafe fn munlock(ptr: *const u8, len: usize) { + VirtualUnlock(ptr as LPVOID, len as SIZE_T); +} + +impl Clone for CryptoVec { + fn clone(&self) -> Self { + let mut v = Self::new(); + v.extend(self); + v + } +} + +impl CryptoVec { + /// Creates a new `CryptoVec`. + pub fn new() -> CryptoVec { + CryptoVec::default() + } + + /// Creates a new `CryptoVec` with `n` zeros. + pub fn new_zeroed(size: usize) -> CryptoVec { + unsafe { + let capacity = size.next_power_of_two(); + let layout = std::alloc::Layout::from_size_align_unchecked(capacity, 1); + let p = std::alloc::alloc_zeroed(layout); + mlock(p, capacity); + CryptoVec { p, capacity, size } + } + } + + /// Creates a new `CryptoVec` with capacity `capacity`. + pub fn with_capacity(capacity: usize) -> CryptoVec { + unsafe { + let capacity = capacity.next_power_of_two(); + let layout = std::alloc::Layout::from_size_align_unchecked(capacity, 1); + let p = std::alloc::alloc_zeroed(layout); + mlock(p, capacity); + CryptoVec { + p, + capacity, + size: 0, + } + } + } + + /// Length of this `CryptoVec`. + /// + /// ``` + /// assert_eq!(cryptovec::CryptoVec::new().len(), 0) + /// ``` + pub fn len(&self) -> usize { + self.size + } + + /// Returns `true` if and only if this CryptoVec is empty. + /// + /// ``` + /// assert!(cryptovec::CryptoVec::new().is_empty()) + /// ``` + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Resize this CryptoVec, appending zeros at the end. This may + /// perform at most one reallocation, overwriting the previous + /// version with zeros. + pub fn resize(&mut self, size: usize) { + if size <= self.capacity && size > self.size { + // If this is an expansion, just resize. + self.size = size + } else if size <= self.size { + // If this is a truncation, resize and erase the extra memory. + unsafe { + libc::memset( + self.p.offset(size as isize) as *mut c_void, + 0, + self.size - size, + ); + } + self.size = size; + } else { + // realloc ! and erase the previous memory. + unsafe { + let next_capacity = size.next_power_of_two(); + let old_ptr = self.p; + let next_layout = std::alloc::Layout::from_size_align_unchecked(next_capacity, 1); + self.p = std::alloc::alloc_zeroed(next_layout); + mlock(self.p, next_capacity); + + if self.capacity > 0 { + std::ptr::copy_nonoverlapping(old_ptr, self.p, self.size); + for i in 0..self.size { + std::ptr::write_volatile(old_ptr.offset(i as isize), 0) + } + munlock(old_ptr, self.capacity); + let layout = std::alloc::Layout::from_size_align_unchecked(self.capacity, 1); + std::alloc::dealloc(old_ptr, layout); + } + + if self.p.is_null() { + panic!("Realloc failed, pointer = {:?} {:?}", self, size) + } else { + self.capacity = next_capacity; + self.size = size; + } + } + } + } + + /// Clear this CryptoVec (retaining the memory). + /// + /// ``` + /// let mut v = cryptovec::CryptoVec::new(); + /// v.extend(b"blabla"); + /// v.clear(); + /// assert!(v.is_empty()) + /// ``` + pub fn clear(&mut self) { + self.resize(0); + } + + /// Append a new byte at the end of this CryptoVec. + pub fn push(&mut self, s: u8) { + let size = self.size; + self.resize(size + 1); + unsafe { *(self.p.offset(size as isize)) = s } + } + + /// Append a new u32, big endian-encoded, at the end of this CryptoVec. + /// + /// ``` + /// let mut v = cryptovec::CryptoVec::new(); + /// let n = 43554; + /// v.push_u32_be(n); + /// assert_eq!(n, v.read_u32_be(0)) + /// ``` + pub fn push_u32_be(&mut self, s: u32) { + let s = s.to_be(); + let x: [u8; 4] = unsafe { std::mem::transmute(s) }; + self.extend(&x) + } + + /// Read a big endian-encoded u32 from this CryptoVec, with the + /// first byte at position `i`. + /// + /// ``` + /// let mut v = cryptovec::CryptoVec::new(); + /// let n = 99485710; + /// v.push_u32_be(n); + /// assert_eq!(n, v.read_u32_be(0)) + /// ``` + pub fn read_u32_be(&self, i: usize) -> u32 { + assert!(i + 4 <= self.size); + let mut x: u32 = 0; + unsafe { + libc::memcpy( + (&mut x) as *mut u32 as *mut c_void, + self.p.offset(i as isize) as *const c_void, + 4, + ); + } + u32::from_be(x) + } + + /// Read `n_bytes` from `r`, and append them at the end of this + /// `CryptoVec`. Returns the number of bytes read (and appended). + pub fn read( + &mut self, + n_bytes: usize, + mut r: R, + ) -> Result { + let cur_size = self.size; + self.resize(cur_size + n_bytes); + let s = + unsafe { std::slice::from_raw_parts_mut(self.p.offset(cur_size as isize), n_bytes) }; + // Resize the buffer to its appropriate size. + match r.read(s) { + Ok(n) => { + self.resize(cur_size + n); + Ok(n) + } + Err(e) => { + self.resize(cur_size); + Err(e) + } + } + } + + /// Write all this CryptoVec to the provided `Write`. Returns the + /// number of bytes actually written. + /// + /// ``` + /// let mut v = cryptovec::CryptoVec::new(); + /// v.extend(b"blabla"); + /// let mut s = std::io::stdout(); + /// v.write_all_from(0, &mut s).unwrap(); + /// ``` + pub fn write_all_from( + &self, + offset: usize, + mut w: W, + ) -> Result { + assert!(offset < self.size); + // if we're past this point, self.p cannot be null. + unsafe { + let s = std::slice::from_raw_parts(self.p.offset(offset as isize), self.size - offset); + w.write(s) + } + } + + /// Resize this CryptoVec, returning a mutable borrow to the extra bytes. + /// + /// ``` + /// let mut v = cryptovec::CryptoVec::new(); + /// v.resize_mut(4).clone_from_slice(b"test"); + /// ``` + pub fn resize_mut(&mut self, n: usize) -> &mut [u8] { + let size = self.size; + self.resize(size + n); + unsafe { std::slice::from_raw_parts_mut(self.p.offset(size as isize), n) } + } + + /// Append a slice at the end of this CryptoVec. + /// + /// ``` + /// let mut v = cryptovec::CryptoVec::new(); + /// v.extend(b"test"); + /// ``` + pub fn extend(&mut self, s: &[u8]) { + let size = self.size; + self.resize(size + s.len()); + unsafe { + std::ptr::copy_nonoverlapping(s.as_ptr(), self.p.offset(size as isize), s.len()); + } + } + + /// Create a `CryptoVec` from a slice + /// + /// ``` + /// CryptoVec::from_slice(b"test"); + /// ``` + pub fn from_slice(s: &[u8]) -> CryptoVec { + let mut v = CryptoVec::new(); + v.resize(s.len()); + unsafe { + std::ptr::copy_nonoverlapping(s.as_ptr(), v.p, s.len()); + } + v + } +} + +impl Drop for CryptoVec { + fn drop(&mut self) { + if self.capacity > 0 { + unsafe { + for i in 0..self.size { + std::ptr::write_volatile(self.p.offset(i as isize), 0) + } + munlock(self.p, self.capacity); + let layout = std::alloc::Layout::from_size_align_unchecked(self.capacity, 1); + std::alloc::dealloc(self.p, layout); + } + } + } +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000..a817cb76 --- /dev/null +++ b/shell.nix @@ -0,0 +1,15 @@ +with import {}; + +let src = fetchFromGitHub { + owner = "mozilla"; + repo = "nixpkgs-mozilla"; + rev = "8c007b60731c07dd7a052cce508de3bb1ae849b4"; + hash = "sha256-RsNPnEKd7BcogwkqhaV5kI/HuNC4flH/OQCC/4W5y/8="; + }; +in +with import "${src.out}/rust-overlay.nix" pkgs pkgs; + +stdenv.mkDerivation { + name = "rust-pijul"; + buildInputs = [ rustChannels.stable.rust libsodium pkgconfig openssl ]; +} diff --git a/thrussh-config/Cargo.toml b/thrussh-config/Cargo.toml new file mode 100644 index 00000000..9d34ba4c --- /dev/null +++ b/thrussh-config/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "thrussh-config" +description = "Utilities to parse .ssh/config files, including helpers to implement ProxyCommand in Thrussh." +version = "0.6.0" +authors = ["Pierre-Étienne Meunier "] +include = [ "Cargo.toml", "src/lib.rs", "src/proxy.rs" ] +license = "Apache-2.0" +documentation = "https://docs.rs/thrussh-config" +repository = "https://nest.pijul.com/pijul/thrussh" +edition = "2018" + +[dependencies] +log = "0.4" +dirs-next = "2.0" +tokio = { version = "1.0", features = [ "io-util", "net", "macros", "process" ] } +futures = "0.3" +thiserror = "1.0" +whoami = "1.0" diff --git a/thrussh-config/src/lib.rs b/thrussh-config/src/lib.rs new file mode 100644 index 00000000..f47c1650 --- /dev/null +++ b/thrussh-config/src/lib.rs @@ -0,0 +1,170 @@ +use log::debug; +use std::io::Read; +use std::net::ToSocketAddrs; +use std::path::Path; +use thiserror::*; + +#[derive(Debug, Error)] +/// anyhow::Errors. +pub enum Error { + #[error("Host not found")] + HostNotFound, + #[error("No home directory")] + NoHome, + #[error("{}", source)] + Io { + #[from] + source: std::io::Error, + }, +} + +mod proxy; +pub use proxy::*; + +#[derive(Debug)] +pub struct Config { + pub user: String, + pub host_name: String, + pub port: u16, + pub identity_file: Option, + pub proxy_command: Option, + pub add_keys_to_agent: AddKeysToAgent, +} + +impl Config { + pub fn default(host_name: &str) -> Self { + Config { + user: whoami::username(), + host_name: host_name.to_string(), + port: 22, + identity_file: None, + proxy_command: None, + add_keys_to_agent: AddKeysToAgent::default(), + } + } +} + +impl Config { + fn update_proxy_command(&mut self) { + if let Some(ref mut prox) = self.proxy_command { + *prox = prox.replace("%h", &self.host_name); + *prox = prox.replace("%p", &format!("{}", self.port)); + } + } + + pub async fn stream(&mut self) -> Result { + self.update_proxy_command(); + if let Some(ref proxy_command) = self.proxy_command { + let cmd: Vec<&str> = proxy_command.split(' ').collect(); + Stream::proxy_command(cmd[0], &cmd[1..]).await + } else { + Stream::tcp_connect( + &(self.host_name.as_str(), self.port) + .to_socket_addrs()? + .next() + .unwrap(), + ) + .await + } + } +} + +pub fn parse_home(host: &str) -> Result { + let mut home = if let Some(home) = dirs_next::home_dir() { + home + } else { + return Err(Error::NoHome); + }; + home.push(".ssh"); + home.push("config"); + parse_path(&home, host) +} + +pub fn parse_path>(path: P, host: &str) -> Result { + let mut s = String::new(); + let mut b = std::fs::File::open(path)?; + b.read_to_string(&mut s)?; + parse(&s, host) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddKeysToAgent { + Yes, + Confirm, + Ask, + No, +} + +impl Default for AddKeysToAgent { + fn default() -> Self { + AddKeysToAgent::No + } +} + +pub fn parse(file: &str, host: &str) -> Result { + let mut config: Option = None; + for line in file.lines() { + let line = line.trim(); + if let Some(n) = line.find(' ') { + let (key, value) = line.split_at(n); + let lower = key.to_lowercase(); + if let Some(ref mut config) = config { + match lower.as_str() { + "host" => break, + "user" => { + config.user.clear(); + config.user.push_str(value.trim_start()); + } + "hostname" => { + config.host_name.clear(); + config.host_name.push_str(value.trim_start()) + } + "port" => { + if let Ok(port) = value.trim_start().parse() { + config.port = port + } + } + "identityfile" => { + let id = value.trim_start(); + if id.starts_with("~/") { + if let Some(mut home) = dirs_next::home_dir() { + home.push(id.split_at(2).1); + config.identity_file = Some(home.to_str().unwrap().to_string()); + } else { + return Err(Error::NoHome); + } + } else { + config.identity_file = Some(id.to_string()) + } + } + "proxycommand" => config.proxy_command = Some(value.trim_start().to_string()), + "addkeystoagent" => match value.to_lowercase().as_str() { + "yes" => config.add_keys_to_agent = AddKeysToAgent::Yes, + "confirm" => config.add_keys_to_agent = AddKeysToAgent::Confirm, + "ask" => config.add_keys_to_agent = AddKeysToAgent::Ask, + _ => config.add_keys_to_agent = AddKeysToAgent::No, + }, + key => { + debug!("{:?}", key); + } + } + } else { + match lower.as_str() { + "host" => { + if value.trim_start() == host { + let mut c = Config::default(host); + c.port = 22; + config = Some(c) + } + } + _ => {} + } + } + } + } + if let Some(config) = config { + Ok(config) + } else { + Err(Error::HostNotFound) + } +} diff --git a/thrussh-config/src/proxy.rs b/thrussh-config/src/proxy.rs new file mode 100644 index 00000000..1d035b66 --- /dev/null +++ b/thrussh-config/src/proxy.rs @@ -0,0 +1,83 @@ +use futures::ready; +use futures::task::*; +use std; +use std::net::SocketAddr; +use std::pin::Pin; +use std::process::Stdio; +use tokio; +use tokio::io::ReadBuf; +use tokio::net::TcpStream; +use tokio::process::Command; + +/// A type to implement either a TCP socket, or proxying through an external command. +pub enum Stream { + #[allow(missing_docs)] + Child(tokio::process::Child), + #[allow(missing_docs)] + Tcp(TcpStream), +} + +impl Stream { + /// Connect a direct TCP stream (as opposed to a proxied one). + pub async fn tcp_connect(addr: &SocketAddr) -> Result { + Ok(Stream::Tcp(tokio::net::TcpStream::connect(addr).await?)) + } + /// Connect through a proxy command. + pub async fn proxy_command(cmd: &str, args: &[&str]) -> Result { + Ok(Stream::Child( + Command::new(cmd) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .args(args) + .spawn() + .unwrap(), + )) + } +} + +impl tokio::io::AsyncRead for Stream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut ReadBuf, + ) -> Poll> { + match *self { + Stream::Child(ref mut c) => Pin::new(c.stdout.as_mut().unwrap()).poll_read(cx, buf), + Stream::Tcp(ref mut t) => Pin::new(t).poll_read(cx, buf), + } + } +} + +impl tokio::io::AsyncWrite for Stream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + match *self { + Stream::Child(ref mut c) => Pin::new(c.stdin.as_mut().unwrap()).poll_write(cx, buf), + Stream::Tcp(ref mut t) => Pin::new(t).poll_write(cx, buf), + } + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + match *self { + Stream::Child(ref mut c) => Pin::new(c.stdin.as_mut().unwrap()).poll_flush(cx), + Stream::Tcp(ref mut t) => Pin::new(t).poll_flush(cx), + } + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context, + ) -> Poll> { + match *self { + Stream::Child(ref mut c) => { + ready!(Pin::new(c.stdin.as_mut().unwrap()).poll_shutdown(cx))?; + drop(c.stdin.take()); + Poll::Ready(Ok(())) + } + Stream::Tcp(ref mut t) => Pin::new(t).poll_shutdown(cx), + } + } +} diff --git a/thrussh-keys/Cargo.toml b/thrussh-keys/Cargo.toml new file mode 100644 index 00000000..05eba3df --- /dev/null +++ b/thrussh-keys/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "thrussh-keys" +version = "0.21.0" +edition = "2018" +authors = ["Pierre-Étienne Meunier "] +description = "Deal with SSH keys: load them, decrypt them, call an SSH agent." +keywords = ["ssh"] +repository = "https://nest.pijul.com/pijul/thrussh" +homepage = "https://pijul.org/thrussh" +documentation = "https://docs.rs/thrussh-keys" +license = "Apache-2.0" +include = [ +"Cargo.toml", +"src/lib.rs", +"src/pem.rs", +"src/agent/mod.rs", +"src/agent/msg.rs", +"src/agent/server.rs", +"src/agent/client.rs", +"src/bcrypt_pbkdf.rs", +"src/blowfish.rs", +"src/encoding.rs", +"src/format/mod.rs", +"src/format/openssh.rs", +"src/format/pkcs5.rs", +"src/format/pkcs8.rs", +"src/key.rs", +"src/signature.rs" +] + +[dependencies] +data-encoding = "2.3" +byteorder = "1.4" +tokio = { version = "1.6", features = [ "io-util", "rt-multi-thread", "time", "net" ] } +tokio-stream = { version = "0.1", features = [ "net" ] } +futures = "0.3" +cryptovec = "0.6.0" +yasna = { version = "0.4.0", features = [ "bit-vec", "num-bigint" ] } +num-bigint = "0.4" +num-integer = "0.1" +openssl = { version = "0.10", optional = true } +bit-vec = "0.6" +serde_derive = "1.0" +serde = "1.0" +dirs = "3.0" +log = "0.4" +thiserror = "1.0" +thrussh-libsodium = "0.2" +sha2 = "0.9" +pbkdf2 = "0.8" +hmac = "0.11" +rand = "0.8" +block-modes = "0.8" +aes = { version = "0.7", features = [ "ctr" ] } +bcrypt-pbkdf = "0.6" +md5 = "0.7" + +[dev-dependencies] +env_logger = "0.8" +tempdir="0.3" diff --git a/thrussh-keys/src/agent/client.rs b/thrussh-keys/src/agent/client.rs new file mode 100644 index 00000000..727e5eae --- /dev/null +++ b/thrussh-keys/src/agent/client.rs @@ -0,0 +1,499 @@ +use super::msg; +use super::Constraint; +use crate::encoding::{Encoding, Reader}; +use crate::key; +use crate::key::{PublicKey, SignatureHash}; +use crate::Error; +use byteorder::{BigEndian, ByteOrder}; +use cryptovec::CryptoVec; +use tokio; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +/// SSH agent client. +pub struct AgentClient { + stream: S, + buf: CryptoVec, +} + +// https://tools.ietf.org/html/draft-miller-ssh-agent-00#section-4.1 +impl AgentClient { + /// Build a future that connects to an SSH agent via the provided + /// stream (on Unix, usually a Unix-domain socket). + pub fn connect(stream: S) -> Self { + AgentClient { + stream, + buf: CryptoVec::new(), + } + } +} + +#[cfg(unix)] +impl AgentClient { + /// Build a future that connects to an SSH agent via the provided + /// stream (on Unix, usually a Unix-domain socket). + pub async fn connect_uds>(path: P) -> Result { + let stream = tokio::net::UnixStream::connect(path).await?; + Ok(AgentClient { + stream, + buf: CryptoVec::new(), + }) + } + + /// Build a future that connects to an SSH agent via the provided + /// stream (on Unix, usually a Unix-domain socket). + pub async fn connect_env() -> Result { + let var = if let Ok(var) = std::env::var("SSH_AUTH_SOCK") { + var + } else { + return Err(Error::EnvVar("SSH_AUTH_SOCK")); + }; + match Self::connect_uds(var).await { + Err(Error::IO(io_err)) if io_err.kind() == std::io::ErrorKind::NotFound => Err(Error::BadAuthSock), + owise => owise + } + } +} + +#[cfg(not(unix))] +impl AgentClient { + /// Build a future that connects to an SSH agent via the provided + /// stream (on Unix, usually a Unix-domain socket). + pub async fn connect_env() -> Result { + Err(Error::AgentFailure) + } +} + +impl AgentClient { + async fn read_response(&mut self) -> Result<(), Error> { + // Writing the message + self.stream.write_all(&self.buf).await?; + self.stream.flush().await?; + + // Reading the length + self.buf.clear(); + self.buf.resize(4); + self.stream.read_exact(&mut self.buf).await?; + + // Reading the rest of the buffer + let len = BigEndian::read_u32(&self.buf) as usize; + self.buf.clear(); + self.buf.resize(len); + self.stream.read_exact(&mut self.buf).await?; + + Ok(()) + } + + /// Send a key to the agent, with a (possibly empty) slice of + /// constraints to apply when using the key to sign. + pub async fn add_identity( + &mut self, + key: &key::KeyPair, + constraints: &[Constraint], + ) -> Result<(), Error> { + self.buf.clear(); + self.buf.resize(4); + if constraints.is_empty() { + self.buf.push(msg::ADD_IDENTITY) + } else { + self.buf.push(msg::ADD_ID_CONSTRAINED) + } + match *key { + key::KeyPair::Ed25519(ref secret) => { + self.buf.extend_ssh_string(b"ssh-ed25519"); + let public = &secret.key[32..]; + self.buf.extend_ssh_string(public); + self.buf.push_u32_be(64); + self.buf.extend(&secret.key); + self.buf.extend_ssh_string(b""); + } + #[cfg(feature = "openssl")] + key::KeyPair::RSA { ref key, .. } => { + self.buf.extend_ssh_string(b"ssh-rsa"); + self.buf.extend_ssh_mpint(&key.n().to_vec()); + self.buf.extend_ssh_mpint(&key.e().to_vec()); + self.buf.extend_ssh_mpint(&key.d().to_vec()); + if let Some(iqmp) = key.iqmp() { + self.buf.extend_ssh_mpint(&iqmp.to_vec()); + } else { + let mut ctx = openssl::bn::BigNumContext::new()?; + let mut iqmp = openssl::bn::BigNum::new()?; + iqmp.mod_inverse(key.p().unwrap(), key.q().unwrap(), &mut ctx)?; + self.buf.extend_ssh_mpint(&iqmp.to_vec()); + } + self.buf.extend_ssh_mpint(&key.p().unwrap().to_vec()); + self.buf.extend_ssh_mpint(&key.q().unwrap().to_vec()); + self.buf.extend_ssh_string(b""); + } + } + if !constraints.is_empty() { + self.buf.push_u32_be(constraints.len() as u32); + for cons in constraints { + match *cons { + Constraint::KeyLifetime { seconds } => { + self.buf.push(msg::CONSTRAIN_LIFETIME); + self.buf.push_u32_be(seconds) + } + Constraint::Confirm => self.buf.push(msg::CONSTRAIN_CONFIRM), + Constraint::Extensions { + ref name, + ref details, + } => { + self.buf.push(msg::CONSTRAIN_EXTENSION); + self.buf.extend_ssh_string(name); + self.buf.extend_ssh_string(details); + } + } + } + } + let len = self.buf.len() - 4; + BigEndian::write_u32(&mut self.buf[..], len as u32); + + self.read_response().await?; + Ok(()) + } + + /// Add a smart card to the agent, with a (possibly empty) set of + /// constraints to apply when signing. + pub async fn add_smartcard_key( + &mut self, + id: &str, + pin: &[u8], + constraints: &[Constraint], + ) -> Result<(), Error> { + self.buf.clear(); + self.buf.resize(4); + if constraints.is_empty() { + self.buf.push(msg::ADD_SMARTCARD_KEY) + } else { + self.buf.push(msg::ADD_SMARTCARD_KEY_CONSTRAINED) + } + self.buf.extend_ssh_string(id.as_bytes()); + self.buf.extend_ssh_string(pin); + if !constraints.is_empty() { + self.buf.push_u32_be(constraints.len() as u32); + for cons in constraints { + match *cons { + Constraint::KeyLifetime { seconds } => { + self.buf.push(msg::CONSTRAIN_LIFETIME); + self.buf.push_u32_be(seconds) + } + Constraint::Confirm => self.buf.push(msg::CONSTRAIN_CONFIRM), + Constraint::Extensions { + ref name, + ref details, + } => { + self.buf.push(msg::CONSTRAIN_EXTENSION); + self.buf.extend_ssh_string(name); + self.buf.extend_ssh_string(details); + } + } + } + } + let len = self.buf.len() - 4; + BigEndian::write_u32(&mut self.buf[0..], len as u32); + self.read_response().await?; + Ok(()) + } + + /// Lock the agent, making it refuse to sign until unlocked. + pub async fn lock(&mut self, passphrase: &[u8]) -> Result<(), Error> { + self.buf.clear(); + self.buf.resize(4); + self.buf.push(msg::LOCK); + self.buf.extend_ssh_string(passphrase); + let len = self.buf.len() - 4; + BigEndian::write_u32(&mut self.buf[0..], len as u32); + self.read_response().await?; + Ok(()) + } + + /// Unlock the agent, allowing it to sign again. + pub async fn unlock(&mut self, passphrase: &[u8]) -> Result<(), Error> { + self.buf.clear(); + self.buf.resize(4); + self.buf.push(msg::UNLOCK); + self.buf.extend_ssh_string(passphrase); + let len = self.buf.len() - 4; + BigEndian::write_u32(&mut self.buf[0..], len as u32); + self.read_response().await?; + Ok(()) + } + + /// Ask the agent for a list of the currently registered secret + /// keys. + pub async fn request_identities(&mut self) -> Result, Error> { + self.buf.clear(); + self.buf.resize(4); + self.buf.push(msg::REQUEST_IDENTITIES); + let len = self.buf.len() - 4; + BigEndian::write_u32(&mut self.buf[0..], len as u32); + + self.read_response().await?; + debug!("identities: {:?}", &self.buf[..]); + let mut keys = Vec::new(); + if self.buf[0] == msg::IDENTITIES_ANSWER { + let mut r = self.buf.reader(1); + let n = r.read_u32()?; + for _ in 0..n { + let key = r.read_string()?; + let _ = r.read_string()?; + let mut r = key.reader(0); + let t = r.read_string()?; + debug!("t = {:?}", std::str::from_utf8(t)); + match t { + #[cfg(feature = "openssl")] + b"ssh-rsa" => { + let e = r.read_mpint()?; + let n = r.read_mpint()?; + use openssl::bn::BigNum; + use openssl::pkey::PKey; + use openssl::rsa::Rsa; + keys.push(PublicKey::RSA { + key: key::OpenSSLPKey(PKey::from_rsa(Rsa::from_public_components( + BigNum::from_slice(n)?, + BigNum::from_slice(e)?, + )?)?), + hash: SignatureHash::SHA2_512, + }) + } + b"ssh-ed25519" => { + let mut p = key::ed25519::PublicKey::new_zeroed(); + p.key.clone_from_slice(r.read_string()?); + keys.push(PublicKey::Ed25519(p)) + } + t => { + info!("Unsupported key type: {:?}", std::str::from_utf8(t)) + } + } + } + } + + Ok(keys) + } + + /// Ask the agent to sign the supplied piece of data. + pub fn sign_request( + mut self, + public: &key::PublicKey, + mut data: CryptoVec, + ) -> impl futures::Future)> { + debug!("sign_request: {:?}", data); + let hash = self.prepare_sign_request(public, &data); + async move { + let resp = self.read_response().await; + debug!("resp = {:?}", &self.buf[..]); + if let Err(e) = resp { + return (self, Err(e)); + } + + if !self.buf.is_empty() && self.buf[0] == msg::SIGN_RESPONSE { + let resp = self.write_signature(hash, &mut data); + if let Err(e) = resp { + return (self, Err(e)); + } + (self, Ok(data)) + } else if self.buf[0] == msg::FAILURE { + (self, Err(Error::AgentFailure.into())) + } else { + debug!("self.buf = {:?}", &self.buf[..]); + (self, Ok(data)) + } + } + } + + fn prepare_sign_request(&mut self, public: &key::PublicKey, data: &[u8]) -> u32 { + self.buf.clear(); + self.buf.resize(4); + self.buf.push(msg::SIGN_REQUEST); + key_blob(&public, &mut self.buf); + self.buf.extend_ssh_string(data); + debug!("public = {:?}", public); + let hash = match public { + #[cfg(feature = "openssl")] + PublicKey::RSA { hash, .. } => match hash { + SignatureHash::SHA2_256 => 2, + SignatureHash::SHA2_512 => 4, + SignatureHash::SHA1 => 0, + }, + _ => 0, + }; + self.buf.push_u32_be(hash); + let len = self.buf.len() - 4; + BigEndian::write_u32(&mut self.buf[0..], len as u32); + hash + } + + fn write_signature(&self, hash: u32, data: &mut CryptoVec) -> Result<(), Error> { + let mut r = self.buf.reader(1); + let mut resp = r.read_string()?.reader(0); + let t = resp.read_string()?; + if (hash == 2 && t == b"rsa-sha2-256") || (hash == 4 && t == b"rsa-sha2-512") || hash == 0 { + let sig = resp.read_string()?; + data.push_u32_be((t.len() + sig.len() + 8) as u32); + data.extend_ssh_string(t); + data.extend_ssh_string(sig); + } + Ok(()) + } + + /// Ask the agent to sign the supplied piece of data. + pub fn sign_request_base64( + mut self, + public: &key::PublicKey, + data: &[u8], + ) -> impl futures::Future)> { + debug!("sign_request: {:?}", data); + self.prepare_sign_request(public, &data); + async move { + let resp = self.read_response().await; + if let Err(e) = resp { + return (self, Err(e)); + } + + if !self.buf.is_empty() && self.buf[0] == msg::SIGN_RESPONSE { + let base64 = data_encoding::BASE64_NOPAD.encode(&self.buf[1..]); + (self, Ok(base64)) + } else { + (self, Ok(String::new())) + } + } + } + + /// Ask the agent to sign the supplied piece of data, and return a `Signature`. + pub fn sign_request_signature( + mut self, + public: &key::PublicKey, + data: &[u8], + ) -> impl futures::Future)> { + debug!("sign_request: {:?}", data); + self.prepare_sign_request(public, data); + + async move { + if let Err(e) = self.read_response().await { + return (self, Err(e)); + } + if !self.buf.is_empty() && self.buf[0] == msg::SIGN_RESPONSE { + let as_sig = |buf: &CryptoVec| -> Result { + let mut r = buf.reader(1); + let mut resp = r.read_string()?.reader(0); + let typ = resp.read_string()?; + let sig = resp.read_string()?; + use crate::signature::Signature; + match typ { + b"rsa-sha2-256" => Ok(Signature::RSA { + bytes: sig.to_vec(), + hash: SignatureHash::SHA2_256, + }), + b"rsa-sha2-512" => Ok(Signature::RSA { + bytes: sig.to_vec(), + hash: SignatureHash::SHA2_512, + }), + b"ssh-ed25519" => { + let mut sig_bytes = [0; 64]; + sig_bytes.clone_from_slice(sig); + Ok(Signature::Ed25519(crate::signature::SignatureBytes( + sig_bytes, + ))) + } + _ => Err((Error::UnknownSignatureType { + sig_type: std::str::from_utf8(typ).unwrap_or("").to_string(), + }) + .into()), + } + }; + let sig = as_sig(&self.buf); + (self, sig) + } else { + (self, Err(Error::AgentProtocolError.into())) + } + } + } + + /// Ask the agent to remove a key from its memory. + pub async fn remove_identity(&mut self, public: &key::PublicKey) -> Result<(), Error> { + self.buf.clear(); + self.buf.resize(4); + self.buf.push(msg::REMOVE_IDENTITY); + key_blob(public, &mut self.buf); + let len = self.buf.len() - 4; + BigEndian::write_u32(&mut self.buf[0..], len as u32); + self.read_response().await?; + Ok(()) + } + + /// Ask the agent to remove a smartcard from its memory. + pub async fn remove_smartcard_key(&mut self, id: &str, pin: &[u8]) -> Result<(), Error> { + self.buf.clear(); + self.buf.resize(4); + self.buf.push(msg::REMOVE_SMARTCARD_KEY); + self.buf.extend_ssh_string(id.as_bytes()); + self.buf.extend_ssh_string(pin); + let len = self.buf.len() - 4; + BigEndian::write_u32(&mut self.buf[0..], len as u32); + self.read_response().await?; + Ok(()) + } + + /// Ask the agent to forget all known keys. + pub async fn remove_all_identities(&mut self) -> Result<(), Error> { + self.buf.clear(); + self.buf.resize(4); + self.buf.push(msg::REMOVE_ALL_IDENTITIES); + BigEndian::write_u32(&mut self.buf[0..], 5); + self.read_response().await?; + Ok(()) + } + + /// Send a custom message to the agent. + pub async fn extension(&mut self, typ: &[u8], ext: &[u8]) -> Result<(), Error> { + self.buf.clear(); + self.buf.resize(4); + self.buf.push(msg::EXTENSION); + self.buf.extend_ssh_string(typ); + self.buf.extend_ssh_string(ext); + let len = self.buf.len() - 4; + BigEndian::write_u32(&mut self.buf[0..], len as u32); + self.read_response().await?; + Ok(()) + } + + /// Ask the agent what extensions about supported extensions. + pub async fn query_extension(&mut self, typ: &[u8], mut ext: CryptoVec) -> Result { + self.buf.clear(); + self.buf.resize(4); + self.buf.push(msg::EXTENSION); + self.buf.extend_ssh_string(typ); + let len = self.buf.len() - 4; + BigEndian::write_u32(&mut self.buf[0..], len as u32); + self.read_response().await?; + + let mut r = self.buf.reader(1); + ext.extend(r.read_string()?); + + Ok(!self.buf.is_empty() && self.buf[0] == msg::SUCCESS) + } +} + +fn key_blob(public: &key::PublicKey, buf: &mut CryptoVec) { + match *public { + #[cfg(feature = "openssl")] + PublicKey::RSA { ref key, .. } => { + buf.extend(&[0, 0, 0, 0]); + let len0 = buf.len(); + buf.extend_ssh_string(b"ssh-rsa"); + let rsa = key.0.rsa().unwrap(); + buf.extend_ssh_mpint(&rsa.e().to_vec()); + buf.extend_ssh_mpint(&rsa.n().to_vec()); + let len1 = buf.len(); + BigEndian::write_u32(&mut buf[5..], (len1 - len0) as u32); + } + PublicKey::Ed25519(ref p) => { + buf.extend(&[0, 0, 0, 0]); + let len0 = buf.len(); + buf.extend_ssh_string(b"ssh-ed25519"); + buf.extend_ssh_string(&p.key[0..]); + let len1 = buf.len(); + BigEndian::write_u32(&mut buf[5..], (len1 - len0) as u32); + } + } +} diff --git a/thrussh-keys/src/agent/mod.rs b/thrussh-keys/src/agent/mod.rs new file mode 100644 index 00000000..d7ec3f6d --- /dev/null +++ b/thrussh-keys/src/agent/mod.rs @@ -0,0 +1,16 @@ +/// Write clients for SSH agents. +pub mod client; +mod msg; +/// Write servers for SSH agents. +pub mod server; + +/// Constraints on how keys can be used +#[derive(Debug, PartialEq, Eq)] +pub enum Constraint { + /// The key shall disappear from the agent's memory after that many seconds. + KeyLifetime { seconds: u32 }, + /// Signatures need to be confirmed by the agent (for instance using a dialog). + Confirm, + /// Custom constraints + Extensions { name: Vec, details: Vec }, +} diff --git a/thrussh-keys/src/agent/msg.rs b/thrussh-keys/src/agent/msg.rs new file mode 100644 index 00000000..a77c5091 --- /dev/null +++ b/thrussh-keys/src/agent/msg.rs @@ -0,0 +1,22 @@ +pub const FAILURE: u8 = 5; +pub const SUCCESS: u8 = 6; +pub const IDENTITIES_ANSWER: u8 = 12; +pub const SIGN_RESPONSE: u8 = 14; +// pub const EXTENSION_FAILURE: u8 = 28; + +pub const REQUEST_IDENTITIES: u8 = 11; +pub const SIGN_REQUEST: u8 = 13; +pub const ADD_IDENTITY: u8 = 17; +pub const REMOVE_IDENTITY: u8 = 18; +pub const REMOVE_ALL_IDENTITIES: u8 = 19; +pub const ADD_ID_CONSTRAINED: u8 = 25; +pub const ADD_SMARTCARD_KEY: u8 = 20; +pub const REMOVE_SMARTCARD_KEY: u8 = 21; +pub const LOCK: u8 = 22; +pub const UNLOCK: u8 = 23; +pub const ADD_SMARTCARD_KEY_CONSTRAINED: u8 = 26; +pub const EXTENSION: u8 = 27; + +pub const CONSTRAIN_LIFETIME: u8 = 1; +pub const CONSTRAIN_CONFIRM: u8 = 2; +pub const CONSTRAIN_EXTENSION: u8 = 3; diff --git a/thrussh-keys/src/agent/server.rs b/thrussh-keys/src/agent/server.rs new file mode 100644 index 00000000..aef4f652 --- /dev/null +++ b/thrussh-keys/src/agent/server.rs @@ -0,0 +1,380 @@ +use crate::encoding::{Encoding, Position, Reader}; +use crate::key; +#[cfg(feature = "openssl")] +use crate::key::SignatureHash; +use byteorder::{BigEndian, ByteOrder}; +use cryptovec::CryptoVec; +use futures::future::Future; +use futures::stream::{Stream, StreamExt}; +use std; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use std::time::Duration; +use std::time::SystemTime; +use tokio; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio::time::sleep; + +use super::msg; +use super::Constraint; +use crate::Error; + +#[derive(Clone)] +struct KeyStore(Arc, (Arc, SystemTime, Vec)>>>); + +#[derive(Clone)] +struct Lock(Arc>); + +#[allow(missing_docs)] +#[derive(Debug)] +pub enum ServerError { + E(E), + Error(Error), +} + +pub trait Agent: Clone + Send + 'static { + fn confirm( + self, + _pk: Arc, + ) -> Box + Unpin + Send> { + Box::new(futures::future::ready((self, true))) + } +} + +pub async fn serve(mut listener: L, agent: A) -> Result<(), Error> +where + S: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static, + L: Stream> + Unpin, + A: Agent + Send + Sync + 'static, +{ + let keys = KeyStore(Arc::new(RwLock::new(HashMap::new()))); + let lock = Lock(Arc::new(RwLock::new(CryptoVec::new()))); + while let Some(Ok(stream)) = listener.next().await { + let mut buf = CryptoVec::new(); + buf.resize(4); + tokio::spawn( + (Connection { + lock: lock.clone(), + keys: keys.clone(), + agent: Some(agent.clone()), + s: stream, + buf: CryptoVec::new(), + }) + .run(), + ); + } + Ok(()) +} + +impl Agent for () { + fn confirm( + self, + _: Arc, + ) -> Box + Unpin + Send> { + Box::new(futures::future::ready((self, true))) + } +} + +struct Connection { + lock: Lock, + keys: KeyStore, + agent: Option, + s: S, + buf: CryptoVec, +} + +impl + Connection +{ + async fn run(mut self) -> Result<(), Error> { + let mut writebuf = CryptoVec::new(); + loop { + // Reading the length + self.buf.clear(); + self.buf.resize(4); + self.s.read_exact(&mut self.buf).await?; + // Reading the rest of the buffer + let len = BigEndian::read_u32(&self.buf) as usize; + self.buf.clear(); + self.buf.resize(len); + self.s.read_exact(&mut self.buf).await?; + // respond + writebuf.clear(); + self.respond(&mut writebuf).await?; + self.s.write_all(&writebuf).await?; + self.s.flush().await? + } + } + + async fn respond(&mut self, writebuf: &mut CryptoVec) -> Result<(), Error> { + let is_locked = { + if let Ok(password) = self.lock.0.read() { + !password.is_empty() + } else { + true + } + }; + writebuf.extend(&[0, 0, 0, 0]); + let mut r = self.buf.reader(0); + match r.read_byte() { + Ok(11) if !is_locked => { + // request identities + if let Ok(keys) = self.keys.0.read() { + writebuf.push(msg::IDENTITIES_ANSWER); + writebuf.push_u32_be(keys.len() as u32); + for (k, _) in keys.iter() { + writebuf.extend_ssh_string(k); + writebuf.extend_ssh_string(b""); + } + } else { + writebuf.push(msg::FAILURE) + } + } + Ok(13) if !is_locked => { + // sign request + let agent = self.agent.take().unwrap(); + let (agent, signed) = self.try_sign(agent, r, writebuf).await?; + self.agent = Some(agent); + if signed { + return Ok(()); + } else { + writebuf.resize(4); + writebuf.push(msg::FAILURE) + } + } + Ok(17) if !is_locked => { + // add identity + if let Ok(true) = self.add_key(r, false, writebuf).await { + } else { + writebuf.push(msg::FAILURE) + } + } + Ok(18) if !is_locked => { + // remove identity + if let Ok(true) = self.remove_identity(r) { + writebuf.push(msg::SUCCESS) + } else { + writebuf.push(msg::FAILURE) + } + } + Ok(19) if !is_locked => { + // remove all identities + if let Ok(mut keys) = self.keys.0.write() { + keys.clear(); + writebuf.push(msg::SUCCESS) + } else { + writebuf.push(msg::FAILURE) + } + } + Ok(22) if !is_locked => { + // lock + if let Ok(()) = self.lock(r) { + writebuf.push(msg::SUCCESS) + } else { + writebuf.push(msg::FAILURE) + } + } + Ok(23) if is_locked => { + // unlock + if let Ok(true) = self.unlock(r) { + writebuf.push(msg::SUCCESS) + } else { + writebuf.push(msg::FAILURE) + } + } + Ok(25) if !is_locked => { + // add identity constrained + if let Ok(true) = self.add_key(r, true, writebuf).await { + } else { + writebuf.push(msg::FAILURE) + } + } + _ => { + // Message not understood + writebuf.push(msg::FAILURE) + } + } + let len = writebuf.len() - 4; + BigEndian::write_u32(&mut writebuf[0..], len as u32); + Ok(()) + } + + fn lock(&self, mut r: Position) -> Result<(), Error> { + let password = r.read_string()?; + let mut lock = self.lock.0.write().unwrap(); + lock.extend(password); + Ok(()) + } + + fn unlock(&self, mut r: Position) -> Result { + let password = r.read_string()?; + let mut lock = self.lock.0.write().unwrap(); + if &lock[0..] == password { + lock.clear(); + Ok(true) + } else { + Ok(false) + } + } + + fn remove_identity(&self, mut r: Position) -> Result { + if let Ok(mut keys) = self.keys.0.write() { + if keys.remove(r.read_string()?).is_some() { + Ok(true) + } else { + Ok(false) + } + } else { + Ok(false) + } + } + + async fn add_key<'a>( + &self, + mut r: Position<'a>, + constrained: bool, + writebuf: &mut CryptoVec, + ) -> Result { + let pos0 = r.position; + let t = r.read_string()?; + let (blob, key) = match t { + b"ssh-ed25519" => { + let public_ = r.read_string()?; + let pos1 = r.position; + let concat = r.read_string()?; + let _comment = r.read_string()?; + if &concat[32..64] != public_ { + return Ok(false); + } + use key::ed25519::*; + let mut public = PublicKey::new_zeroed(); + let mut secret = SecretKey::new_zeroed(); + public.key.clone_from_slice(&public_[..32]); + secret.key.clone_from_slice(&concat[..]); + writebuf.push(msg::SUCCESS); + (self.buf[pos0..pos1].to_vec(), key::KeyPair::Ed25519(secret)) + } + #[cfg(feature = "openssl")] + b"ssh-rsa" => { + use openssl::bn::{BigNum, BigNumContext}; + use openssl::rsa::Rsa; + let n = r.read_mpint()?; + let e = r.read_mpint()?; + let d = BigNum::from_slice(r.read_mpint()?)?; + let q_inv = r.read_mpint()?; + let p = BigNum::from_slice(r.read_mpint()?)?; + let q = BigNum::from_slice(r.read_mpint()?)?; + let (dp, dq) = { + let one = BigNum::from_u32(1)?; + let p1 = p.as_ref() - one.as_ref(); + let q1 = q.as_ref() - one.as_ref(); + let mut context = BigNumContext::new()?; + let mut dp = BigNum::new()?; + let mut dq = BigNum::new()?; + dp.checked_rem(&d, &p1, &mut context)?; + dq.checked_rem(&d, &q1, &mut context)?; + (dp, dq) + }; + let _comment = r.read_string()?; + let key = Rsa::from_private_components( + BigNum::from_slice(n)?, + BigNum::from_slice(e)?, + d, + p, + q, + dp, + dq, + BigNum::from_slice(&q_inv)?, + )?; + + let len0 = writebuf.len(); + writebuf.extend_ssh_string(b"ssh-rsa"); + writebuf.extend_ssh_mpint(&e); + writebuf.extend_ssh_mpint(&n); + let blob = writebuf[len0..].to_vec(); + writebuf.resize(len0); + writebuf.push(msg::SUCCESS); + ( + blob, + key::KeyPair::RSA { + key, + hash: SignatureHash::SHA2_256, + }, + ) + } + _ => return Ok(false), + }; + let mut w = self.keys.0.write().unwrap(); + let now = SystemTime::now(); + if constrained { + let n = r.read_u32()?; + let mut c = Vec::new(); + for _ in 0..n { + let t = r.read_byte()?; + if t == msg::CONSTRAIN_LIFETIME { + let seconds = r.read_u32()?; + c.push(Constraint::KeyLifetime { seconds }); + let blob = blob.clone(); + let keys = self.keys.clone(); + tokio::spawn(async move { + sleep(Duration::from_secs(seconds as u64)).await; + let mut keys = keys.0.write().unwrap(); + let delete = if let Some(&(_, time, _)) = keys.get(&blob) { + time == now + } else { + false + }; + if delete { + keys.remove(&blob); + } + }); + } else if t == msg::CONSTRAIN_CONFIRM { + c.push(Constraint::Confirm) + } else { + return Ok(false); + } + } + w.insert(blob, (Arc::new(key), now, Vec::new())); + } else { + w.insert(blob, (Arc::new(key), now, Vec::new())); + } + Ok(true) + } + + async fn try_sign<'a>( + &self, + agent: A, + mut r: Position<'a>, + writebuf: &mut CryptoVec, + ) -> Result<(A, bool), Error> { + let mut needs_confirm = false; + let key = { + let blob = r.read_string()?; + let k = self.keys.0.read().unwrap(); + if let Some(&(ref key, _, ref constraints)) = k.get(blob) { + if constraints.iter().any(|c| *c == Constraint::Confirm) { + needs_confirm = true; + } + key.clone() + } else { + return Ok((agent, false)); + } + }; + let agent = if needs_confirm { + let (agent, ok) = agent.confirm(key.clone()).await; + if !ok { + return Ok((agent, false)); + } + agent + } else { + agent + }; + writebuf.push(msg::SIGN_RESPONSE); + let data = r.read_string()?; + key.add_signature(writebuf, data)?; + let len = writebuf.len(); + BigEndian::write_u32(writebuf, (len - 4) as u32); + + Ok((agent, true)) + } +} diff --git a/thrussh-keys/src/bcrypt_pbkdf.rs b/thrussh-keys/src/bcrypt_pbkdf.rs new file mode 100644 index 00000000..67491b18 --- /dev/null +++ b/thrussh-keys/src/bcrypt_pbkdf.rs @@ -0,0 +1,88 @@ +// From the rust-crypto project. + +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use crate::blowfish::*; +use byteorder::{BigEndian, ByteOrder, LittleEndian}; +use sha2::{Sha512, Digest}; + +fn bcrypt_hash(hpass: &[u8], hsalt: &[u8], output: &mut [u8; 32]) { + let mut bf = Blowfish::init_state(); + bf.salted_expand_key(hsalt, hpass); + + for _ in 0..64 { + bf.expand_key(hsalt); + bf.expand_key(hpass); + } + + // b"OxychromaticBlowfishSwatDynamite" + let mut buf: [u32; 8] = [ + 1333295459, 1752330093, 1635019107, 1114402679, 1718186856, 1400332660, 1148808801, + 1835627621, + ]; + let mut i = 0; + while i < 8 { + for _ in 0..64 { + let (l, r) = bf.encrypt(buf[i], buf[i + 1]); + buf[i] = l; + buf[i + 1] = r; + } + i += 2 + } + + for i in 0..8 { + LittleEndian::write_u32(&mut output[i * 4..(i + 1) * 4], buf[i]); + } +} + +pub fn bcrypt_pbkdf(password: &[u8], salt: &[u8], rounds: u32, output: &mut [u8]) { + assert!(password.len() > 0); + assert!(salt.len() > 0); + assert!(rounds > 0); + assert!(output.len() > 0); + assert!(output.len() <= 1024); + + let nblocks = (output.len() + 31) / 32; + + let hpass = { + let mut hasher = Sha512::new(); + hasher.update(password); + hasher.finalize() + }; + + for block in 1..(nblocks + 1) { + let mut count = [0u8; 4]; + let mut out = [0u8; 32]; + BigEndian::write_u32(&mut count, block as u32); + + let mut hasher = Sha512::new(); + hasher.update(salt); + hasher.update(&count); + let hsalt = hasher.finalize(); + + bcrypt_hash(hpass.as_ref(), hsalt.as_ref(), &mut out); + let mut tmp = out; + + for _ in 1..rounds { + let mut hasher = sha2::Sha512::new(); + hasher.update(&tmp); + let hsalt = hasher.finalize(); + + bcrypt_hash(hpass.as_ref(), hsalt.as_ref(), &mut tmp); + for i in 0..out.len() { + out[i] ^= tmp[i]; + } + + for i in 0..out.len() { + let idx = i * nblocks + (block - 1); + if idx < output.len() { + output[idx] = out[i]; + } + } + } + } +} diff --git a/thrussh-keys/src/blowfish.rs b/thrussh-keys/src/blowfish.rs new file mode 100644 index 00000000..dcb256a8 --- /dev/null +++ b/thrussh-keys/src/blowfish.rs @@ -0,0 +1,317 @@ +// From the rust-crypto project. + +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[derive(Clone, Copy)] +pub struct Blowfish { + s: [[u32; 256]; 4], + p: [u32; 18], +} + +fn next_u32_wrap(buf: &[u8], offset: &mut usize) -> u32 { + let mut v = 0; + for _ in 0..4 { + if *offset >= buf.len() { + *offset = 0; + } + v = (v << 8) | buf[*offset] as u32; + *offset += 1; + } + v +} + +impl Blowfish { + // For bcrypt. Use Blowfish::new instead. + pub fn init_state() -> Blowfish { + Blowfish { + p: [ + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, + 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7, 0xc97c50dd, + 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b, + ], + s: [ + [ + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, + 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, + 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, + 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, + 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, + 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, + 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, + 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, + 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, + 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, + 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, + 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, + 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, + 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, + 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, + 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, + 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, + 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, + 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, + 0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, + 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, + 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, + ], + [ + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, + 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, + 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, + 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, + 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, + 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, + 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, + 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, + 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, + 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, + 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, + 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, + 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, + 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, + 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, + 0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, + 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, + 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646, + 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, + 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, + 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, + 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, + ], + [ + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, + 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, + 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, + 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, + 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, + 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, + 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, + 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, + 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, + 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, + 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, + 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, + 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, + 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086, + 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, + 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, + 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, + 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, + 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, + 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, + 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, + 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, + ], + [ + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, + 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, + 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, + 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, + 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, + 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, + 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, + 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, + 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, + 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, + 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, + 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, + 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, + 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, + 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, + 0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, + 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, + 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, + 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, + 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, + 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, + 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6, + ], + ], + } + } + + // For bcrypt. Use Blowfish::new instead. + pub fn expand_key(&mut self, key: &[u8]) { + let mut key_pos = 0; + for i in 0..18 { + self.p[i] ^= next_u32_wrap(key, &mut key_pos); + } + let mut l = 0u32; + let mut r = 0u32; + let mut i = 0; + while i < 18 { + let (new_l, new_r) = self.encrypt(l, r); + l = new_l; + r = new_r; + self.p[i] = l; + self.p[i + 1] = r; + i += 2 + } + for i in 0..4 { + let mut j = 0; + while j < 256 { + let (new_l, new_r) = self.encrypt(l, r); + l = new_l; + r = new_r; + self.s[i][j] = l; + self.s[i][j + 1] = r; + j += 2 + } + } + } + + // Bcrypt key schedule. + pub fn salted_expand_key(&mut self, salt: &[u8], key: &[u8]) { + let mut key_pos = 0; + for i in 0..18 { + self.p[i] ^= next_u32_wrap(key, &mut key_pos); + } + let mut l = 0u32; + let mut r = 0u32; + let mut salt_pos = 0; + let mut i = 0; + while i < 18 { + let (new_l, new_r) = self.encrypt( + l ^ next_u32_wrap(salt, &mut salt_pos), + r ^ next_u32_wrap(salt, &mut salt_pos), + ); + l = new_l; + r = new_r; + self.p[i] = l; + self.p[i + 1] = r; + i += 2 + } + for i in 0..4 { + let mut j = 0; + while j < 256 { + let (new_l, new_r) = self.encrypt( + l ^ next_u32_wrap(salt, &mut salt_pos), + r ^ next_u32_wrap(salt, &mut salt_pos), + ); + l = new_l; + r = new_r; + self.s[i][j] = l; + self.s[i][j + 1] = r; + + let (new_l, new_r) = self.encrypt( + l ^ next_u32_wrap(salt, &mut salt_pos), + r ^ next_u32_wrap(salt, &mut salt_pos), + ); + l = new_l; + r = new_r; + self.s[i][j + 2] = l; + self.s[i][j + 3] = r; + j += 4 + } + } + } + + fn round_function(&self, x: u32) -> u32 { + ((self.s[0][(x >> 24) as usize].wrapping_add(self.s[1][((x >> 16) & 0xff) as usize])) + ^ self.s[2][((x >> 8) & 0xff) as usize]) + .wrapping_add(self.s[3][(x & 0xff) as usize]) + } + + // Public for bcrypt. + pub fn encrypt(&self, mut l: u32, mut r: u32) -> (u32, u32) { + let mut i = 0; + while i < 16 { + l ^= self.p[i]; + r ^= self.round_function(l); + r ^= self.p[i + 1]; + l ^= self.round_function(r); + i += 2 + } + l ^= self.p[16]; + r ^= self.p[17]; + (r, l) + } +} diff --git a/thrussh-keys/src/encoding.rs b/thrussh-keys/src/encoding.rs new file mode 100644 index 00000000..b48591c6 --- /dev/null +++ b/thrussh-keys/src/encoding.rs @@ -0,0 +1,228 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use crate::Error; +use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; +use cryptovec::CryptoVec; + +#[doc(hidden)] +pub trait Bytes { + fn bytes(&self) -> &[u8]; +} + +impl> Bytes for A { + fn bytes(&self) -> &[u8] { + self.as_ref().as_bytes() + } +} + +/// Encode in the SSH format. +pub trait Encoding { + /// Push an SSH-encoded string to `self`. + fn extend_ssh_string(&mut self, s: &[u8]); + /// Push an SSH-encoded blank string of length `s` to `self`. + fn extend_ssh_string_blank(&mut self, s: usize) -> &mut [u8]; + /// Push an SSH-encoded multiple-precision integer. + fn extend_ssh_mpint(&mut self, s: &[u8]); + /// Push an SSH-encoded list. + fn extend_list>(&mut self, list: I); + /// Push an SSH-encoded empty list. + fn write_empty_list(&mut self); +} + +/// Encoding length of the given mpint. +pub fn mpint_len(s: &[u8]) -> usize { + let mut i = 0; + while i < s.len() && s[i] == 0 { + i += 1 + } + (if s[i] & 0x80 != 0 { 5 } else { 4 }) + s.len() - i +} + +impl Encoding for Vec { + fn extend_ssh_string(&mut self, s: &[u8]) { + self.write_u32::(s.len() as u32).unwrap(); + self.extend(s); + } + fn extend_ssh_string_blank(&mut self, len: usize) -> &mut [u8] { + self.write_u32::(len as u32).unwrap(); + let current = self.len(); + self.resize(current + len, 0u8); + &mut self[current..] + } + fn extend_ssh_mpint(&mut self, s: &[u8]) { + // Skip initial 0s. + let mut i = 0; + while i < s.len() && s[i] == 0 { + i += 1 + } + // If the first non-zero is >= 128, write its length (u32, BE), followed by 0. + if s[i] & 0x80 != 0 { + self.write_u32::((s.len() - i + 1) as u32) + .unwrap(); + self.push(0) + } else { + self.write_u32::((s.len() - i) as u32).unwrap(); + } + self.extend(&s[i..]); + } + + fn extend_list>(&mut self, list: I) { + let len0 = self.len(); + self.extend(&[0, 0, 0, 0]); + let mut first = true; + for i in list { + if !first { + self.push(b',') + } else { + first = false; + } + self.extend(i.bytes()) + } + let len = (self.len() - len0 - 4) as u32; + + BigEndian::write_u32(&mut self[len0..], len); + } + + fn write_empty_list(&mut self) { + self.extend(&[0, 0, 0, 0]); + } +} + +impl Encoding for CryptoVec { + fn extend_ssh_string(&mut self, s: &[u8]) { + self.push_u32_be(s.len() as u32); + self.extend(s); + } + fn extend_ssh_string_blank(&mut self, len: usize) -> &mut [u8] { + self.push_u32_be(len as u32); + let current = self.len(); + self.resize(current + len); + &mut self[current..] + } + fn extend_ssh_mpint(&mut self, s: &[u8]) { + // Skip initial 0s. + let mut i = 0; + while i < s.len() && s[i] == 0 { + i += 1 + } + // If the first non-zero is >= 128, write its length (u32, BE), followed by 0. + if s[i] & 0x80 != 0 { + self.push_u32_be((s.len() - i + 1) as u32); + self.push(0) + } else { + self.push_u32_be((s.len() - i) as u32); + } + self.extend(&s[i..]); + } + + fn extend_list>(&mut self, list: I) { + let len0 = self.len(); + self.extend(&[0, 0, 0, 0]); + let mut first = true; + for i in list { + if !first { + self.push(b',') + } else { + first = false; + } + self.extend(i.bytes()) + } + let len = (self.len() - len0 - 4) as u32; + + BigEndian::write_u32(&mut self[len0..], len); + } + + fn write_empty_list(&mut self) { + self.extend(&[0, 0, 0, 0]); + } +} + +/// A cursor-like trait to read SSH-encoded things. +pub trait Reader { + /// Create an SSH reader for `self`. + fn reader<'a>(&'a self, starting_at: usize) -> Position<'a>; +} + +impl Reader for CryptoVec { + fn reader<'a>(&'a self, starting_at: usize) -> Position<'a> { + Position { + s: &self, + position: starting_at, + } + } +} + +impl Reader for [u8] { + fn reader<'a>(&'a self, starting_at: usize) -> Position<'a> { + Position { + s: self, + position: starting_at, + } + } +} + +/// A cursor-like type to read SSH-encoded values. +#[derive(Debug)] +pub struct Position<'a> { + s: &'a [u8], + #[doc(hidden)] + pub position: usize, +} +impl<'a> Position<'a> { + /// Read one string from this reader. + pub fn read_string(&mut self) -> Result<&'a [u8], Error> { + let len = self.read_u32()? as usize; + if self.position + len <= self.s.len() { + let result = &self.s[self.position..(self.position + len)]; + self.position += len; + Ok(result) + } else { + Err(Error::IndexOutOfBounds) + } + } + /// Read a `u32` from this reader. + pub fn read_u32(&mut self) -> Result { + if self.position + 4 <= self.s.len() { + let u = BigEndian::read_u32(&self.s[self.position..]); + self.position += 4; + Ok(u) + } else { + Err(Error::IndexOutOfBounds) + } + } + /// Read one byte from this reader. + pub fn read_byte(&mut self) -> Result { + if self.position + 1 <= self.s.len() { + let u = self.s[self.position]; + self.position += 1; + Ok(u) + } else { + Err(Error::IndexOutOfBounds) + } + } + + /// Read one byte from this reader. + pub fn read_mpint(&mut self) -> Result<&'a [u8], Error> { + let len = self.read_u32()? as usize; + if self.position + len <= self.s.len() { + let result = &self.s[self.position..(self.position + len)]; + self.position += len; + Ok(result) + } else { + Err(Error::IndexOutOfBounds) + } + } +} diff --git a/thrussh-keys/src/format/mod.rs b/thrussh-keys/src/format/mod.rs new file mode 100644 index 00000000..8623a223 --- /dev/null +++ b/thrussh-keys/src/format/mod.rs @@ -0,0 +1,147 @@ +use super::is_base64_char; +use crate::key; +use crate::Error; +#[cfg(not(feature = "openssl"))] +use data_encoding::BASE64_MIME; +#[cfg(feature = "openssl")] +use data_encoding::{BASE64_MIME, HEXLOWER_PERMISSIVE}; + +#[cfg(feature = "openssl")] +use openssl::rsa::Rsa; +use std::io::Write; + +pub mod openssh; +pub use self::openssh::*; + +#[cfg(feature = "openssl")] +pub mod pkcs5; +#[cfg(feature = "openssl")] +pub use self::pkcs5::*; + +pub mod pkcs8; + +const AES_128_CBC: &'static str = "DEK-Info: AES-128-CBC,"; + +#[derive(Clone, Copy, Debug)] +/// AES encryption key. +pub enum Encryption { + /// Key for AES128 + Aes128Cbc([u8; 16]), + /// Key for AES256 + Aes256Cbc([u8; 16]), +} + +#[derive(Clone, Debug)] +enum Format { + #[cfg(feature = "openssl")] + Rsa, + Openssh, + #[cfg(feature = "openssl")] + Pkcs5Encrypted(Encryption), + Pkcs8Encrypted, + Pkcs8, +} + +/// Decode a secret key, possibly deciphering it with the supplied +/// password. +pub fn decode_secret_key(secret: &str, password: Option<&str>) -> Result { + let mut format = None; + let secret = { + let mut started = false; + let mut sec = String::new(); + for l in secret.lines() { + if started == true { + if l.starts_with("-----END ") { + break; + } + if l.chars().all(is_base64_char) { + sec.push_str(l) + } else if l.starts_with(AES_128_CBC) { + #[cfg(feature = "openssl")] + { + let iv_: Vec = HEXLOWER_PERMISSIVE + .decode(l.split_at(AES_128_CBC.len()).1.as_bytes())?; + if iv_.len() != 16 { + return Err(Error::CouldNotReadKey.into()); + } + let mut iv = [0; 16]; + iv.clone_from_slice(&iv_); + format = Some(Format::Pkcs5Encrypted(Encryption::Aes128Cbc(iv))) + } + } + } + if l == "-----BEGIN OPENSSH PRIVATE KEY-----" { + started = true; + format = Some(Format::Openssh); + } else if l == "-----BEGIN RSA PRIVATE KEY-----" && cfg!(feature = "openssl") { + #[cfg(feature = "openssl")] + { + started = true; + format = Some(Format::Rsa); + } + } else if l == "-----BEGIN ENCRYPTED PRIVATE KEY-----" { + started = true; + format = Some(Format::Pkcs8Encrypted); + } else if l == "-----BEGIN PRIVATE KEY-----" { + started = true; + format = Some(Format::Pkcs8); + } + } + sec + }; + + let secret = BASE64_MIME.decode(secret.as_bytes())?; + match format { + Some(Format::Openssh) => decode_openssh(&secret, password), + #[cfg(feature = "openssl")] + Some(Format::Rsa) => decode_rsa(&secret), + #[cfg(feature = "openssl")] + Some(Format::Pkcs5Encrypted(enc)) => decode_pkcs5(&secret, password, enc), + Some(Format::Pkcs8Encrypted) | Some(Format::Pkcs8) => { + self::pkcs8::decode_pkcs8(&secret, password.map(|x| x.as_bytes())) + } + None => Err(Error::CouldNotReadKey.into()), + } +} + +pub fn encode_pkcs8_pem(key: &key::KeyPair, mut w: W) -> Result<(), Error> { + let x = self::pkcs8::encode_pkcs8(key); + w.write_all(b"-----BEGIN PRIVATE KEY-----\n")?; + w.write_all(BASE64_MIME.encode(&x).as_bytes())?; + w.write_all(b"\n-----END PRIVATE KEY-----\n")?; + Ok(()) +} + +pub fn encode_pkcs8_pem_encrypted( + key: &key::KeyPair, + pass: &[u8], + rounds: u32, + mut w: W, +) -> Result<(), Error> { + let x = self::pkcs8::encode_pkcs8_encrypted(pass, rounds, key)?; + w.write_all(b"-----BEGIN ENCRYPTED PRIVATE KEY-----\n")?; + w.write_all(BASE64_MIME.encode(&x).as_bytes())?; + w.write_all(b"\n-----END ENCRYPTED PRIVATE KEY-----\n")?; + Ok(()) +} + +#[cfg(feature = "openssl")] +fn decode_rsa(secret: &[u8]) -> Result { + Ok(key::KeyPair::RSA { + key: Rsa::private_key_from_der(secret)?, + hash: key::SignatureHash::SHA2_256, + }) +} + +fn pkcs_unpad(dec: &mut Vec) { + let len = dec.len(); + if len > 0 { + let padding_len = dec[len - 1]; + if dec[(len - padding_len as usize)..] + .iter() + .all(|&x| x == padding_len) + { + dec.truncate(len - padding_len as usize) + } + } +} diff --git a/thrussh-keys/src/format/openssh.rs b/thrussh-keys/src/format/openssh.rs new file mode 100644 index 00000000..d3f33244 --- /dev/null +++ b/thrussh-keys/src/format/openssh.rs @@ -0,0 +1,152 @@ +use crate::encoding::Reader; +use crate::key; +use crate::{Error, KEYTYPE_ED25519, KEYTYPE_RSA}; +use bcrypt_pbkdf; +#[cfg(feature = "openssl")] +use openssl::bn::BigNum; + +/// Decode a secret key given in the OpenSSH format, deciphering it if +/// needed using the supplied password. +pub fn decode_openssh(secret: &[u8], password: Option<&str>) -> Result { + if &secret[0..15] == b"openssh-key-v1\0" { + let mut position = secret.reader(15); + + let ciphername = position.read_string()?; + let kdfname = position.read_string()?; + let kdfoptions = position.read_string()?; + + let nkeys = position.read_u32()?; + + // Read all public keys + for _ in 0..nkeys { + position.read_string()?; + } + + // Read all secret keys + let secret_ = position.read_string()?; + let secret = decrypt_secret_key(ciphername, kdfname, kdfoptions, password, secret_)?; + let mut position = secret.reader(0); + let _check0 = position.read_u32()?; + let _check1 = position.read_u32()?; + for _ in 0..nkeys { + let key_type = position.read_string()?; + if key_type == KEYTYPE_ED25519 { + let pubkey = position.read_string()?; + let seckey = position.read_string()?; + let _comment = position.read_string()?; + assert_eq!(pubkey, &seckey[32..]); + use key::ed25519::*; + let mut secret = SecretKey::new_zeroed(); + secret.key.clone_from_slice(seckey); + return Ok(key::KeyPair::Ed25519(secret)); + } else if key_type == KEYTYPE_RSA && cfg!(feature = "openssl") { + #[cfg(feature = "openssl")] + { + let n = BigNum::from_slice(position.read_string()?)?; + let e = BigNum::from_slice(position.read_string()?)?; + let d = BigNum::from_slice(position.read_string()?)?; + let iqmp = BigNum::from_slice(position.read_string()?)?; + let p = BigNum::from_slice(position.read_string()?)?; + let q = BigNum::from_slice(position.read_string()?)?; + + let mut ctx = openssl::bn::BigNumContext::new()?; + let un = openssl::bn::BigNum::from_u32(1)?; + let mut p1 = openssl::bn::BigNum::new()?; + let mut q1 = openssl::bn::BigNum::new()?; + p1.checked_sub(&p, &un)?; + q1.checked_sub(&q, &un)?; + let mut dmp1 = openssl::bn::BigNum::new()?; // d mod p-1 + dmp1.checked_rem(&d, &p1, &mut ctx)?; + let mut dmq1 = openssl::bn::BigNum::new()?; // d mod q-1 + dmq1.checked_rem(&d, &q1, &mut ctx)?; + + let key = openssl::rsa::RsaPrivateKeyBuilder::new(n, e, d)? + .set_factors(p, q)? + .set_crt_params(dmp1, dmq1, iqmp)? + .build(); + key.check_key().unwrap(); + return Ok(key::KeyPair::RSA { + key, + hash: key::SignatureHash::SHA2_512, + }); + } + } else { + return Err(Error::UnsupportedKeyType(key_type.to_vec()).into()); + } + } + Err(Error::CouldNotReadKey.into()) + } else { + Err(Error::CouldNotReadKey.into()) + } +} + +use aes::*; +use block_modes::block_padding::NoPadding; +type Aes128Cbc = block_modes::Cbc; +type Aes256Cbc = block_modes::Cbc; + +fn decrypt_secret_key( + ciphername: &[u8], + kdfname: &[u8], + kdfoptions: &[u8], + password: Option<&str>, + secret_key: &[u8], +) -> Result, Error> { + if kdfname == b"none" { + if password.is_none() { + Ok(secret_key.to_vec()) + } else { + Err(Error::CouldNotReadKey.into()) + } + } else if let Some(password) = password { + let mut key = [0; 48]; + let n = match ciphername { + b"aes128-cbc" | b"aes128-ctr" => 32, + b"aes256-cbc" | b"aes256-ctr" => 48, + _ => return Err(Error::CouldNotReadKey.into()), + }; + match kdfname { + b"bcrypt" => { + let mut kdfopts = kdfoptions.reader(0); + let salt = kdfopts.read_string()?; + let rounds = kdfopts.read_u32()?; + bcrypt_pbkdf::bcrypt_pbkdf(password, salt, rounds, &mut key[..n]).unwrap(); + } + _kdfname => { + return Err(Error::CouldNotReadKey.into()); + } + }; + let (key, iv) = key.split_at(n - 16); + + let mut dec = secret_key.to_vec(); + dec.resize(dec.len() + 32, 0u8); + use aes::cipher::{NewCipher, StreamCipher}; + use block_modes::BlockMode; + match ciphername { + b"aes128-cbc" => { + let cipher = Aes128Cbc::new_from_slices(key, iv).unwrap(); + let n = cipher.decrypt(&mut dec)?.len(); + dec.truncate(n) + } + b"aes256-cbc" => { + let cipher = Aes256Cbc::new_from_slices(key, iv).unwrap(); + let n = cipher.decrypt(&mut dec)?.len(); + dec.truncate(n) + } + b"aes128-ctr" => { + let mut cipher = Aes128Ctr::new_from_slices(key, iv).unwrap(); + cipher.apply_keystream(&mut dec); + dec.truncate(secret_key.len()) + } + b"aes256-ctr" => { + let mut cipher = Aes256Ctr::new_from_slices(key, iv).unwrap(); + cipher.apply_keystream(&mut dec); + dec.truncate(secret_key.len()) + } + _ => {} + } + Ok(dec) + } else { + Err(Error::KeyIsEncrypted.into()) + } +} diff --git a/thrussh-keys/src/format/pkcs5.rs b/thrussh-keys/src/format/pkcs5.rs new file mode 100644 index 00000000..193b888e --- /dev/null +++ b/thrussh-keys/src/format/pkcs5.rs @@ -0,0 +1,38 @@ +use super::{pkcs_unpad, Encryption}; +use crate::key; +use crate::Error; + +use aes::*; +use block_modes::block_padding::NoPadding; +use block_modes::BlockMode; +type Aes128Cbc = block_modes::Cbc; + +/// Decode a secret key in the PKCS#5 format, possible deciphering it +/// using the supplied password. +#[cfg(feature = "openssl")] +pub fn decode_pkcs5( + secret: &[u8], + password: Option<&str>, + enc: Encryption, +) -> Result { + if let Some(pass) = password { + let sec = match enc { + Encryption::Aes128Cbc(ref iv) => { + let mut c = md5::Context::new(); + c.consume(pass.as_bytes()); + c.consume(&iv[..8]); + let md5 = c.compute(); + + let c = Aes128Cbc::new_from_slices(&md5.0, &iv[..]).unwrap(); + let mut dec = secret.to_vec(); + c.decrypt(&mut dec).unwrap(); + pkcs_unpad(&mut dec); + dec + } + Encryption::Aes256Cbc(_) => unimplemented!(), + }; + super::decode_rsa(&sec) + } else { + Err(Error::KeyIsEncrypted.into()) + } +} diff --git a/thrussh-keys/src/format/pkcs8.rs b/thrussh-keys/src/format/pkcs8.rs new file mode 100644 index 00000000..55b88c37 --- /dev/null +++ b/thrussh-keys/src/format/pkcs8.rs @@ -0,0 +1,449 @@ +use super::{pkcs_unpad, Encryption}; +use crate::key; +#[cfg(feature = "openssl")] +use crate::key::SignatureHash; +use crate::Error; +use bit_vec::BitVec; +#[cfg(feature = "openssl")] +use openssl::pkey::Private; +#[cfg(feature = "openssl")] +use openssl::rsa::Rsa; +use std; +use std::borrow::Cow; +use yasna; +use yasna::BERReaderSeq; + +const PBES2: &'static [u64] = &[1, 2, 840, 113549, 1, 5, 13]; +const PBKDF2: &'static [u64] = &[1, 2, 840, 113549, 1, 5, 12]; +const HMAC_SHA256: &'static [u64] = &[1, 2, 840, 113549, 2, 9]; +const AES256CBC: &'static [u64] = &[2, 16, 840, 1, 101, 3, 4, 1, 42]; +const ED25519: &'static [u64] = &[1, 3, 101, 112]; +#[cfg(feature = "openssl")] +const RSA: &'static [u64] = &[1, 2, 840, 113549, 1, 1, 1]; + +/// Decode a PKCS#8-encoded private key. +pub fn decode_pkcs8(ciphertext: &[u8], password: Option<&[u8]>) -> Result { + let secret = if let Some(pass) = password { + Cow::Owned(yasna::parse_der(&ciphertext, |reader| { + reader.read_sequence(|reader| { + // Encryption parameters + let parameters = reader.next().read_sequence(|reader| { + let oid = reader.next().read_oid()?; + if oid.components().as_slice() == PBES2 { + asn1_read_pbes2(reader) + } else { + Ok(Err(Error::UnknownAlgorithm(oid)).into()) + } + })?; + // Ciphertext + let ciphertext = reader.next().read_bytes()?; + Ok(parameters.map(|p| p.decrypt(pass, &ciphertext))) + }) + })???) + } else { + Cow::Borrowed(ciphertext) + }; + yasna::parse_der(&secret, |reader| { + reader.read_sequence(|reader| { + let version = reader.next().read_u64()?; + if version == 0 { + Ok(read_key_v0(reader)) + } else if version == 1 { + Ok(read_key_v1(reader)) + } else { + Ok(Err(Error::CouldNotReadKey.into())) + } + }) + })? +} + +fn asn1_read_pbes2( + reader: &mut yasna::BERReaderSeq, +) -> Result, yasna::ASN1Error> { + reader.next().read_sequence(|reader| { + // PBES2 has two components. + // 1. Key generation algorithm + let keygen = reader.next().read_sequence(|reader| { + let oid = reader.next().read_oid()?; + if oid.components().as_slice() == PBKDF2 { + asn1_read_pbkdf2(reader) + } else { + Ok(Err(Error::UnknownAlgorithm(oid))) + } + })?; + // 2. Encryption algorithm. + let algorithm = reader.next().read_sequence(|reader| { + let oid = reader.next().read_oid()?; + if oid.components().as_slice() == AES256CBC { + asn1_read_aes256cbc(reader) + } else { + Ok(Err(Error::UnknownAlgorithm(oid))) + } + })?; + Ok(keygen.and_then(|keygen| algorithm.map(|algo| Algorithms::Pbes2(keygen, algo)))) + }) +} + +fn asn1_read_pbkdf2( + reader: &mut yasna::BERReaderSeq, +) -> Result, yasna::ASN1Error> { + reader.next().read_sequence(|reader| { + let salt = reader.next().read_bytes()?; + let rounds = reader.next().read_u64()?; + let digest = reader.next().read_sequence(|reader| { + let oid = reader.next().read_oid()?; + if oid.components().as_slice() == HMAC_SHA256 { + reader.next().read_null()?; + Ok(Ok(())) + } else { + Ok(Err(Error::UnknownAlgorithm(oid))) + } + })?; + Ok(digest.map(|()| KeyDerivation::Pbkdf2 { + salt, + rounds, + })) + }) +} + +fn asn1_read_aes256cbc( + reader: &mut yasna::BERReaderSeq, +) -> Result, yasna::ASN1Error> { + let iv = reader.next().read_bytes()?; + let mut i = [0; 16]; + i.clone_from_slice(&iv); + Ok(Ok(Encryption::Aes256Cbc(i))) +} + +fn write_key_v1(writer: &mut yasna::DERWriterSeq, secret: &key::ed25519::SecretKey) { + writer.next().write_u32(1); + // write OID + writer.next().write_sequence(|writer| { + writer + .next() + .write_oid(&ObjectIdentifier::from_slice(ED25519)); + }); + let seed = yasna::construct_der(|writer| writer.write_bytes(&secret.key)); + writer.next().write_bytes(&seed); + writer + .next() + .write_tagged(yasna::Tag::context(1), |writer| { + let public = &secret.key[32..]; + writer.write_bitvec(&BitVec::from_bytes(&public)) + }) +} + +fn read_key_v1(reader: &mut BERReaderSeq) -> Result { + let oid = reader + .next() + .read_sequence(|reader| reader.next().read_oid())?; + if oid.components().as_slice() == ED25519 { + use key::ed25519::{PublicKey, SecretKey}; + let secret = { + let mut seed = SecretKey::new_zeroed(); + let s = yasna::parse_der(&reader.next().read_bytes()?, |reader| reader.read_bytes())?; + clone(&s, &mut seed.key); + seed + }; + let _public = { + let public = reader + .next() + .read_tagged(yasna::Tag::context(1), |reader| reader.read_bitvec())? + .to_bytes(); + let mut p = PublicKey::new_zeroed(); + clone(&public, &mut p.key); + p + }; + Ok(key::KeyPair::Ed25519(secret)) + } else { + Err(Error::CouldNotReadKey.into()) + } +} + +#[cfg(feature = "openssl")] +fn write_key_v0(writer: &mut yasna::DERWriterSeq, key: &Rsa) { + writer.next().write_u32(0); + // write OID + writer.next().write_sequence(|writer| { + writer.next().write_oid(&ObjectIdentifier::from_slice(RSA)); + writer.next().write_null() + }); + let bytes = yasna::construct_der(|writer| { + writer.write_sequence(|writer| { + writer.next().write_u32(0); + use num_bigint::BigUint; + writer + .next() + .write_biguint(&BigUint::from_bytes_be(&key.n().to_vec())); + writer + .next() + .write_biguint(&BigUint::from_bytes_be(&key.e().to_vec())); + writer + .next() + .write_biguint(&BigUint::from_bytes_be(&key.d().to_vec())); + writer + .next() + .write_biguint(&BigUint::from_bytes_be(&key.p().unwrap().to_vec())); + writer + .next() + .write_biguint(&BigUint::from_bytes_be(&key.q().unwrap().to_vec())); + writer + .next() + .write_biguint(&BigUint::from_bytes_be(&key.dmp1().unwrap().to_vec())); + writer + .next() + .write_biguint(&BigUint::from_bytes_be(&key.dmq1().unwrap().to_vec())); + writer + .next() + .write_biguint(&BigUint::from_bytes_be(&key.iqmp().unwrap().to_vec())); + }) + }); + writer.next().write_bytes(&bytes); +} + +#[cfg(feature = "openssl")] +fn read_key_v0(reader: &mut BERReaderSeq) -> Result { + let oid = reader.next().read_sequence(|reader| { + let oid = reader.next().read_oid()?; + reader.next().read_null()?; + Ok(oid) + })?; + if oid.components().as_slice() == RSA { + let seq = &reader.next().read_bytes()?; + let rsa: Result, Error> = yasna::parse_der(seq, |reader| { + reader.read_sequence(|reader| { + let version = reader.next().read_u32()?; + if version != 0 { + return Ok(Err(Error::CouldNotReadKey.into())); + } + use openssl::bn::BigNum; + let mut read_key = || -> Result, Error> { + Ok(Rsa::from_private_components( + BigNum::from_slice(&reader.next().read_biguint()?.to_bytes_be())?, + BigNum::from_slice(&reader.next().read_biguint()?.to_bytes_be())?, + BigNum::from_slice(&reader.next().read_biguint()?.to_bytes_be())?, + BigNum::from_slice(&reader.next().read_biguint()?.to_bytes_be())?, + BigNum::from_slice(&reader.next().read_biguint()?.to_bytes_be())?, + BigNum::from_slice(&reader.next().read_biguint()?.to_bytes_be())?, + BigNum::from_slice(&reader.next().read_biguint()?.to_bytes_be())?, + BigNum::from_slice(&reader.next().read_biguint()?.to_bytes_be())?, + )?) + }; + Ok(read_key()) + }) + })?; + Ok(key::KeyPair::RSA { + key: rsa?, + hash: SignatureHash::SHA2_256, + }) + } else { + Err(Error::CouldNotReadKey.into()) + } +} + +#[cfg(not(feature = "openssl"))] +fn read_key_v0(_: &mut BERReaderSeq) -> Result { + Err(Error::CouldNotReadKey.into()) +} + +#[test] +fn test_read_write_pkcs8() { + let (public, secret) = key::ed25519::keypair(); + assert_eq!(&public.key, &secret.key[32..]); + let key = key::KeyPair::Ed25519(secret); + let password = b"blabla"; + let ciphertext = encode_pkcs8_encrypted(password, 100, &key).unwrap(); + let key = decode_pkcs8(&ciphertext, Some(password)).unwrap(); + match key { + key::KeyPair::Ed25519 { .. } => println!("Ed25519"), + #[cfg(feature = "openssl")] + key::KeyPair::RSA { .. } => println!("RSA"), + } +} + +use yasna::models::ObjectIdentifier; +use aes::*; +use block_modes::block_padding::NoPadding; +use block_modes::BlockMode; +type Aes128Cbc = block_modes::Cbc; +type Aes256Cbc = block_modes::Cbc; + +/// Encode a password-protected PKCS#8-encoded private key. +pub fn encode_pkcs8_encrypted( + pass: &[u8], + rounds: u32, + key: &key::KeyPair, +) -> Result, Error> { + use rand::RngCore; + let mut rng = rand::thread_rng(); + let mut salt = [0; 64]; + rng.fill_bytes(&mut salt); + let mut iv = [0; 16]; + rng.fill_bytes(&mut iv); + let mut dkey = [0; 32]; // AES256-CBC + pbkdf2::pbkdf2::>(pass, &salt, rounds, &mut dkey); + let mut plaintext = encode_pkcs8(key); + + let padding_len = 32 - (plaintext.len() % 32); + plaintext.extend(std::iter::repeat(padding_len as u8).take(padding_len)); + + let c = Aes256Cbc::new_from_slices(&dkey, &iv).unwrap(); + let n = plaintext.len(); + c.encrypt(&mut plaintext, n).unwrap(); + + Ok(yasna::construct_der(|writer| { + writer.write_sequence(|writer| { + // Encryption parameters + writer.next().write_sequence(|writer| { + writer + .next() + .write_oid(&ObjectIdentifier::from_slice(PBES2)); + asn1_write_pbes2(writer.next(), rounds as u64, &salt, &iv) + }); + // Ciphertext + writer.next().write_bytes(&plaintext[..]) + }) + })) +} + +/// Encode a Decode a PKCS#8-encoded private key. +pub fn encode_pkcs8(key: &key::KeyPair) -> Vec { + yasna::construct_der(|writer| { + writer.write_sequence(|writer| match *key { + key::KeyPair::Ed25519(ref secret) => write_key_v1(writer, secret), + #[cfg(feature = "openssl")] + key::KeyPair::RSA { ref key, .. } => write_key_v0(writer, key), + }) + }) +} + +fn clone(src: &[u8], dest: &mut [u8]) { + let i = src.iter().take_while(|b| **b == 0).count(); + let src = &src[i..]; + let l = dest.len(); + (&mut dest[l - src.len()..]).clone_from_slice(src) +} + +fn asn1_write_pbes2(writer: yasna::DERWriter, rounds: u64, salt: &[u8], iv: &[u8]) { + writer.write_sequence(|writer| { + // 1. Key generation algorithm + writer.next().write_sequence(|writer| { + writer + .next() + .write_oid(&ObjectIdentifier::from_slice(PBKDF2)); + asn1_write_pbkdf2(writer.next(), rounds, salt) + }); + // 2. Encryption algorithm. + writer.next().write_sequence(|writer| { + writer + .next() + .write_oid(&ObjectIdentifier::from_slice(AES256CBC)); + writer.next().write_bytes(iv) + }); + }) +} + +fn asn1_write_pbkdf2(writer: yasna::DERWriter, rounds: u64, salt: &[u8]) { + writer.write_sequence(|writer| { + writer.next().write_bytes(salt); + writer.next().write_u64(rounds); + writer.next().write_sequence(|writer| { + writer + .next() + .write_oid(&ObjectIdentifier::from_slice(HMAC_SHA256)); + writer.next().write_null() + }) + }) +} + +enum Algorithms { + Pbes2(KeyDerivation, Encryption), +} + +impl Algorithms { + fn decrypt(&self, password: &[u8], cipher: &[u8]) -> Result, Error> { + match *self { + Algorithms::Pbes2(ref der, ref enc) => { + let mut key = enc.key(); + der.derive(password, &mut key)?; + let out = enc.decrypt(&key, cipher)?; + Ok(out) + } + } + } +} + +impl KeyDerivation { + fn derive(&self, password: &[u8], key: &mut [u8]) -> Result<(), Error> { + match *self { + KeyDerivation::Pbkdf2 { + ref salt, + rounds, + } => { + pbkdf2::pbkdf2::>(password, salt, rounds as u32, key) + // pbkdf2_hmac(password, salt, rounds as usize, digest, key)? + }, + } + Ok(()) + } +} + +#[derive(Debug)] +enum Key { + K128([u8; 16]), + K256([u8; 32]), +} + +impl std::ops::Deref for Key { + type Target = [u8]; + fn deref(&self) -> &[u8] { + match *self { + Key::K128(ref k) => k, + Key::K256(ref k) => k, + } + } +} + +impl std::ops::DerefMut for Key { + fn deref_mut(&mut self) -> &mut [u8] { + match *self { + Key::K128(ref mut k) => k, + Key::K256(ref mut k) => k, + } + } +} + +impl Encryption { + fn key(&self) -> Key { + match *self { + Encryption::Aes128Cbc(_) => Key::K128([0; 16]), + Encryption::Aes256Cbc(_) => Key::K256([0; 32]), + } + } + + fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result, Error> { + match *self { + Encryption::Aes128Cbc(ref iv) => { + let c = Aes128Cbc::new_from_slices(key, iv).unwrap(); + let mut dec = ciphertext.to_vec(); + c.decrypt(&mut dec)?; + pkcs_unpad(&mut dec); + Ok(dec) + }, + Encryption::Aes256Cbc(ref iv) => { + let c = Aes256Cbc::new_from_slices(key, iv).unwrap(); + let mut dec = ciphertext.to_vec(); + c.decrypt(&mut dec)?; + pkcs_unpad(&mut dec); + Ok(dec) + }, + } + } +} + +enum KeyDerivation { + Pbkdf2 { + salt: Vec, + rounds: u64, + }, +} diff --git a/thrussh-keys/src/key.rs b/thrussh-keys/src/key.rs new file mode 100644 index 00000000..08249bc7 --- /dev/null +++ b/thrussh-keys/src/key.rs @@ -0,0 +1,461 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +use crate::encoding::{Encoding, Reader}; +pub use crate::signature::*; +use crate::Error; +use cryptovec::CryptoVec; +#[cfg(feature = "openssl")] +use openssl::pkey::{Private, Public}; +use thrussh_libsodium as sodium; + +/// Keys for elliptic curve Ed25519 cryptography. +pub mod ed25519 { + pub use thrussh_libsodium::ed25519::{ + keypair, sign_detached, verify_detached, PublicKey, SecretKey, + }; +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +/// Name of a public key algorithm. +pub struct Name(pub &'static str); + +impl AsRef for Name { + fn as_ref(&self) -> &str { + self.0 + } +} + +/// The name of the Ed25519 algorithm for SSH. +pub const ED25519: Name = Name("ssh-ed25519"); +/// The name of the ssh-sha2-512 algorithm for SSH. +pub const RSA_SHA2_512: Name = Name("rsa-sha2-512"); +/// The name of the ssh-sha2-256 algorithm for SSH. +pub const RSA_SHA2_256: Name = Name("rsa-sha2-256"); + +pub const SSH_RSA: Name = Name("ssh-rsa"); + +impl Name { + /// Base name of the private key file for a key name. + pub fn identity_file(&self) -> &'static str { + match *self { + ED25519 => "id_ed25519", + RSA_SHA2_512 => "id_rsa", + RSA_SHA2_256 => "id_rsa", + _ => unreachable!(), + } + } +} + +#[doc(hidden)] +pub trait Verify { + fn verify_client_auth(&self, buffer: &[u8], sig: &[u8]) -> bool; + fn verify_server_auth(&self, buffer: &[u8], sig: &[u8]) -> bool; +} + +/// The hash function used for hashing buffers. +#[derive(Eq, PartialEq, Clone, Copy, Debug, Hash, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum SignatureHash { + /// SHA2, 256 bits. + SHA2_256, + /// SHA2, 512 bits. + SHA2_512, + /// SHA1 + SHA1, +} + +impl SignatureHash { + pub fn name(&self) -> Name { + match *self { + SignatureHash::SHA2_256 => RSA_SHA2_256, + SignatureHash::SHA2_512 => RSA_SHA2_512, + SignatureHash::SHA1 => SSH_RSA, + } + } + + #[cfg(feature = "openssl")] + fn to_message_digest(&self) -> openssl::hash::MessageDigest { + use openssl::hash::MessageDigest; + match *self { + SignatureHash::SHA2_256 => MessageDigest::sha256(), + SignatureHash::SHA2_512 => MessageDigest::sha512(), + SignatureHash::SHA1 => MessageDigest::sha1(), + } + } +} + +/// Public key +#[derive(Eq, PartialEq, Debug)] +pub enum PublicKey { + #[doc(hidden)] + Ed25519(thrussh_libsodium::ed25519::PublicKey), + #[doc(hidden)] + #[cfg(feature = "openssl")] + RSA { + key: OpenSSLPKey, + hash: SignatureHash, + }, +} + +/// A public key from OpenSSL. +#[cfg(feature = "openssl")] +pub struct OpenSSLPKey(pub openssl::pkey::PKey); + +#[cfg(feature = "openssl")] +use std::cmp::{Eq, PartialEq}; +#[cfg(feature = "openssl")] +impl PartialEq for OpenSSLPKey { + fn eq(&self, b: &OpenSSLPKey) -> bool { + self.0.public_eq(&b.0) + } +} +#[cfg(feature = "openssl")] +impl Eq for OpenSSLPKey {} +#[cfg(feature = "openssl")] +impl std::fmt::Debug for OpenSSLPKey { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "OpenSSLPKey {{ (hidden) }}") + } +} + +impl PublicKey { + /// Parse a public key in SSH format. + pub fn parse(algo: &[u8], pubkey: &[u8]) -> Result { + match algo { + b"ssh-ed25519" => { + let mut p = pubkey.reader(0); + let key_algo = p.read_string()?; + let key_bytes = p.read_string()?; + if key_algo != b"ssh-ed25519" || key_bytes.len() != sodium::ed25519::PUBLICKEY_BYTES + { + return Err(Error::CouldNotReadKey.into()); + } + let mut p = sodium::ed25519::PublicKey { + key: [0; sodium::ed25519::PUBLICKEY_BYTES], + }; + p.key.clone_from_slice(key_bytes); + Ok(PublicKey::Ed25519(p)) + } + b"ssh-rsa" | b"rsa-sha2-256" | b"rsa-sha2-512" if cfg!(feature = "openssl") => { + #[cfg(feature = "openssl")] + { + let mut p = pubkey.reader(0); + let key_algo = p.read_string()?; + debug!("{:?}", std::str::from_utf8(key_algo)); + if key_algo != b"ssh-rsa" && key_algo != b"rsa-sha2-256" && key_algo != b"rsa-sha2-512" { + return Err(Error::CouldNotReadKey.into()); + } + let key_e = p.read_string()?; + let key_n = p.read_string()?; + use openssl::bn::BigNum; + use openssl::pkey::PKey; + use openssl::rsa::Rsa; + Ok(PublicKey::RSA { + key: OpenSSLPKey(PKey::from_rsa(Rsa::from_public_components( + BigNum::from_slice(key_n)?, + BigNum::from_slice(key_e)?, + )?)?), + hash: { + if algo == b"rsa-sha2-256" { + SignatureHash::SHA2_256 + } else if algo == b"rsa-sha2-512" { + SignatureHash::SHA2_512 + } else { + SignatureHash::SHA1 + } + }, + }) + } + #[cfg(not(feature = "openssl"))] + { + unreachable!() + } + } + _ => Err(Error::CouldNotReadKey.into()), + } + } + + /// Algorithm name for that key. + pub fn name(&self) -> &'static str { + match *self { + PublicKey::Ed25519(_) => ED25519.0, + #[cfg(feature = "openssl")] + PublicKey::RSA { ref hash, .. } => hash.name().0, + } + } + + /// Verify a signature. + pub fn verify_detached(&self, buffer: &[u8], sig: &[u8]) -> bool { + match self { + &PublicKey::Ed25519(ref public) => { + sodium::ed25519::verify_detached(&sig, buffer, &public) + } + #[cfg(feature = "openssl")] + &PublicKey::RSA { ref key, ref hash } => { + use openssl::sign::*; + let verify = || { + let mut verifier = Verifier::new(hash.to_message_digest(), &key.0)?; + verifier.update(buffer)?; + verifier.verify(&sig) + }; + verify().unwrap_or(false) + } + } + } + + /// Compute the key fingerprint, hashed with sha2-256. + pub fn fingerprint(&self) -> String { + use super::PublicKeyBase64; + let key = self.public_key_bytes(); + use sha2::{Sha256, Digest}; + let mut hasher = Sha256::new(); + hasher.update(&key[..]); + data_encoding::BASE64_NOPAD.encode(&hasher.finalize()) + } + + + #[cfg(feature = "openssl")] + pub fn set_algorithm(&mut self, algorithm: &[u8]) { + if let PublicKey::RSA { ref mut hash, .. } = self { + if algorithm == b"rsa-sha2-512" { + *hash = SignatureHash::SHA2_512 + } else if algorithm == b"rsa-sha2-256" { + *hash = SignatureHash::SHA2_256 + } else if algorithm == b"ssh-rsa" { + *hash = SignatureHash::SHA1 + } + } + } + + #[cfg(not(feature = "openssl"))] + pub fn set_algorithm(&mut self, _: &[u8]) { + } +} + +impl Verify for PublicKey { + fn verify_client_auth(&self, buffer: &[u8], sig: &[u8]) -> bool { + self.verify_detached(buffer, sig) + } + fn verify_server_auth(&self, buffer: &[u8], sig: &[u8]) -> bool { + self.verify_detached(buffer, sig) + } +} + +/// Public key exchange algorithms. +pub enum KeyPair { + Ed25519(sodium::ed25519::SecretKey), + #[cfg(feature = "openssl")] + RSA { + key: openssl::rsa::Rsa, + hash: SignatureHash, + }, +} + +impl std::fmt::Debug for KeyPair { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + KeyPair::Ed25519(ref key) => write!( + f, + "Ed25519 {{ public: {:?}, secret: (hidden) }}", + &key.key[32..] + ), + #[cfg(feature = "openssl")] + KeyPair::RSA { .. } => write!(f, "RSA {{ (hidden) }}"), + } + } +} + +impl<'b> crate::encoding::Bytes for &'b KeyPair { + fn bytes(&self) -> &[u8] { + self.name().as_bytes() + } +} + +impl KeyPair { + /// Copy the public key of this algorithm. + pub fn clone_public_key(&self) -> PublicKey { + match self { + &KeyPair::Ed25519(ref key) => { + let mut public = sodium::ed25519::PublicKey { key: [0; 32] }; + public.key.clone_from_slice(&key.key[32..]); + PublicKey::Ed25519(public) + } + #[cfg(feature = "openssl")] + &KeyPair::RSA { ref key, ref hash } => { + use openssl::pkey::PKey; + use openssl::rsa::Rsa; + let key = Rsa::from_public_components( + key.n().to_owned().unwrap(), + key.e().to_owned().unwrap(), + ) + .unwrap(); + PublicKey::RSA { + key: OpenSSLPKey(PKey::from_rsa(key).unwrap()), + hash: hash.clone(), + } + } + } + } + + /// Name of this key algorithm. + pub fn name(&self) -> &'static str { + match *self { + KeyPair::Ed25519(_) => ED25519.0, + #[cfg(feature = "openssl")] + KeyPair::RSA { ref hash, .. } => hash.name().0, + } + } + + /// Generate a key pair. + pub fn generate_ed25519() -> Option { + let (public, secret) = sodium::ed25519::keypair(); + assert_eq!(&public.key, &secret.key[32..]); + Some(KeyPair::Ed25519(secret)) + } + + #[cfg(feature = "openssl")] + pub fn generate_rsa(bits: usize, hash: SignatureHash) -> Option { + let key = openssl::rsa::Rsa::generate(bits as u32).ok()?; + Some(KeyPair::RSA { key, hash }) + } + + /// Sign a slice using this algorithm. + pub fn sign_detached(&self, to_sign: &[u8]) -> Result { + match self { + &KeyPair::Ed25519(ref secret) => Ok(Signature::Ed25519(SignatureBytes( + sodium::ed25519::sign_detached(to_sign.as_ref(), secret).0, + ))), + + #[cfg(feature = "openssl")] + &KeyPair::RSA { ref key, ref hash } => Ok(Signature::RSA { + bytes: rsa_signature(hash, key, to_sign.as_ref())?, + hash: *hash, + }), + } + } + + #[doc(hidden)] + /// This is used by the server to sign the initial DH kex + /// message. Note: we are not signing the same kind of thing as in + /// the function below, `add_self_signature`. + pub fn add_signature>( + &self, + buffer: &mut CryptoVec, + to_sign: H, + ) -> Result<(), Error> { + match self { + &KeyPair::Ed25519(ref secret) => { + let signature = sodium::ed25519::sign_detached(to_sign.as_ref(), secret); + + buffer.push_u32_be((ED25519.0.len() + signature.0.len() + 8) as u32); + buffer.extend_ssh_string(ED25519.0.as_bytes()); + buffer.extend_ssh_string(&signature.0); + } + #[cfg(feature = "openssl")] + &KeyPair::RSA { ref key, ref hash } => { + // https://tools.ietf.org/html/draft-rsa-dsa-sha2-256-02#section-2.2 + let signature = rsa_signature(hash, key, to_sign.as_ref())?; + let name = hash.name(); + buffer.push_u32_be((name.0.len() + signature.len() + 8) as u32); + buffer.extend_ssh_string(name.0.as_bytes()); + buffer.extend_ssh_string(&signature); + } + } + Ok(()) + } + + #[doc(hidden)] + /// This is used by the client for authentication. Note: we are + /// not signing the same kind of thing as in the above function, + /// `add_signature`. + pub fn add_self_signature(&self, buffer: &mut CryptoVec) -> Result<(), Error> { + match self { + &KeyPair::Ed25519(ref secret) => { + let signature = sodium::ed25519::sign_detached(&buffer, secret); + buffer.push_u32_be((ED25519.0.len() + signature.0.len() + 8) as u32); + buffer.extend_ssh_string(ED25519.0.as_bytes()); + buffer.extend_ssh_string(&signature.0); + } + #[cfg(feature = "openssl")] + &KeyPair::RSA { ref key, ref hash } => { + // https://tools.ietf.org/html/draft-rsa-dsa-sha2-256-02#section-2.2 + let signature = rsa_signature(hash, key, buffer)?; + let name = hash.name(); + buffer.push_u32_be((name.0.len() + signature.len() + 8) as u32); + buffer.extend_ssh_string(name.0.as_bytes()); + buffer.extend_ssh_string(&signature); + } + } + Ok(()) + } +} + +#[cfg(feature = "openssl")] +fn rsa_signature( + hash: &SignatureHash, + key: &openssl::rsa::Rsa, + b: &[u8], +) -> Result, Error> { + use openssl::pkey::*; + use openssl::rsa::*; + use openssl::sign::Signer; + let pkey = PKey::from_rsa(Rsa::from_private_components( + key.n().to_owned()?, + key.e().to_owned()?, + key.d().to_owned()?, + key.p().unwrap().to_owned()?, + key.q().unwrap().to_owned()?, + key.dmp1().unwrap().to_owned()?, + key.dmq1().unwrap().to_owned()?, + key.iqmp().unwrap().to_owned()?, + )?)?; + let mut signer = Signer::new(hash.to_message_digest(), &pkey)?; + signer.update(b)?; + Ok(signer.sign_to_vec()?) +} + +/// Parse a public key from a byte slice. +pub fn parse_public_key(p: &[u8]) -> Result { + let mut pos = p.reader(0); + let t = pos.read_string()?; + if t == b"ssh-ed25519" { + if let Ok(pubkey) = pos.read_string() { + use thrussh_libsodium::ed25519; + let mut p = ed25519::PublicKey { + key: [0; ed25519::PUBLICKEY_BYTES], + }; + p.key.clone_from_slice(pubkey); + return Ok(PublicKey::Ed25519(p)); + } + } + if t == b"ssh-rsa" { + #[cfg(feature = "openssl")] + { + let e = pos.read_string()?; + let n = pos.read_string()?; + use openssl::bn::*; + use openssl::pkey::*; + use openssl::rsa::*; + return Ok(PublicKey::RSA { + key: OpenSSLPKey(PKey::from_rsa(Rsa::from_public_components( + BigNum::from_slice(n)?, + BigNum::from_slice(e)?, + )?)?), + hash: SignatureHash::SHA2_256, + }); + } + } + Err(Error::CouldNotReadKey.into()) +} diff --git a/thrussh-keys/src/lib.rs b/thrussh-keys/src/lib.rs new file mode 100644 index 00000000..3677c475 --- /dev/null +++ b/thrussh-keys/src/lib.rs @@ -0,0 +1,885 @@ +#![deny(trivial_casts, unstable_features, unused_import_braces)] +//! This crate contains methods to deal with SSH keys, as defined in +//! crate Thrussh. This includes in particular various functions for +//! opening key files, deciphering encrypted keys, and dealing with +//! agents. +//! +//! The following example (which uses the `openssl` feature) shows how +//! to do all these in a single example: start and SSH agent server, +//! connect to it with a client, decipher an encrypted private key +//! (the password is `b"blabla"`), send it to the agent, and ask the +//! agent to sign a piece of data (`b"Please sign this", below). +//! +//!``` +//! use thrussh_keys::*; +//! use futures::Future; +//! +//! #[derive(Clone)] +//! struct X{} +//! impl agent::server::Agent for X { +//! fn confirm(self, _: std::sync::Arc) -> Box + Send + Unpin> { +//! Box::new(futures::future::ready((self, true))) +//! } +//! } +//! +//! const PKCS8_ENCRYPTED: &'static str = "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQITo1O0b8YrS0CAggA\nMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBtLH4T1KOfo1GGr7salhR8BIIE\n0KN9ednYwcTGSX3hg7fROhTw7JAJ1D4IdT1fsoGeNu2BFuIgF3cthGHe6S5zceI2\nMpkfwvHbsOlDFWMUIAb/VY8/iYxhNmd5J6NStMYRC9NC0fVzOmrJqE1wITqxtORx\nIkzqkgFUbaaiFFQPepsh5CvQfAgGEWV329SsTOKIgyTj97RxfZIKA+TR5J5g2dJY\nj346SvHhSxJ4Jc0asccgMb0HGh9UUDzDSql0OIdbnZW5KzYJPOx+aDqnpbz7UzY/\nP8N0w/pEiGmkdkNyvGsdttcjFpOWlLnLDhtLx8dDwi/sbEYHtpMzsYC9jPn3hnds\nTcotqjoSZ31O6rJD4z18FOQb4iZs3MohwEdDd9XKblTfYKM62aQJWH6cVQcg+1C7\njX9l2wmyK26Tkkl5Qg/qSfzrCveke5muZgZkFwL0GCcgPJ8RixSB4GOdSMa/hAMU\nkvFAtoV2GluIgmSe1pG5cNMhurxM1dPPf4WnD+9hkFFSsMkTAuxDZIdDk3FA8zof\nYhv0ZTfvT6V+vgH3Hv7Tqcxomy5Qr3tj5vvAqqDU6k7fC4FvkxDh2mG5ovWvc4Nb\nXv8sed0LGpYitIOMldu6650LoZAqJVv5N4cAA2Edqldf7S2Iz1QnA/usXkQd4tLa\nZ80+sDNv9eCVkfaJ6kOVLk/ghLdXWJYRLenfQZtVUXrPkaPpNXgD0dlaTN8KuvML\nUw/UGa+4ybnPsdVflI0YkJKbxouhp4iB4S5ACAwqHVmsH5GRnujf10qLoS7RjDAl\no/wSHxdT9BECp7TT8ID65u2mlJvH13iJbktPczGXt07nBiBse6OxsClfBtHkRLzE\nQF6UMEXsJnIIMRfrZQnduC8FUOkfPOSXc8r9SeZ3GhfbV/DmWZvFPCpjzKYPsM5+\nN8Bw/iZ7NIH4xzNOgwdp5BzjH9hRtCt4sUKVVlWfEDtTnkHNOusQGKu7HkBF87YZ\nRN/Nd3gvHob668JOcGchcOzcsqsgzhGMD8+G9T9oZkFCYtwUXQU2XjMN0R4VtQgZ\nrAxWyQau9xXMGyDC67gQ5xSn+oqMK0HmoW8jh2LG/cUowHFAkUxdzGadnjGhMOI2\nzwNJPIjF93eDF/+zW5E1l0iGdiYyHkJbWSvcCuvTwma9FIDB45vOh5mSR+YjjSM5\nnq3THSWNi7Cxqz12Q1+i9pz92T2myYKBBtu1WDh+2KOn5DUkfEadY5SsIu/Rb7ub\n5FBihk2RN3y/iZk+36I69HgGg1OElYjps3D+A9AjVby10zxxLAz8U28YqJZm4wA/\nT0HLxBiVw+rsHmLP79KvsT2+b4Diqih+VTXouPWC/W+lELYKSlqnJCat77IxgM9e\nYIhzD47OgWl33GJ/R10+RDoDvY4koYE+V5NLglEhbwjloo9Ryv5ywBJNS7mfXMsK\n/uf+l2AscZTZ1mhtL38efTQCIRjyFHc3V31DI0UdETADi+/Omz+bXu0D5VvX+7c6\nb1iVZKpJw8KUjzeUV8yOZhvGu3LrQbhkTPVYL555iP1KN0Eya88ra+FUKMwLgjYr\nJkUx4iad4dTsGPodwEP/Y9oX/Qk3ZQr+REZ8lg6IBoKKqqrQeBJ9gkm1jfKE6Xkc\nCog3JMeTrb3LiPHgN6gU2P30MRp6L1j1J/MtlOAr5rux\n-----END ENCRYPTED PRIVATE KEY-----\n"; +//! +//! fn main() { +//! env_logger::try_init().unwrap_or(()); +//! let dir = tempdir::TempDir::new("thrussh").unwrap(); +//! let agent_path = dir.path().join("agent"); +//! +//! let mut core = tokio::runtime::Runtime::new().unwrap(); +//! let agent_path_ = agent_path.clone(); +//! // Starting a server +//! core.spawn(async move { +//! let mut listener = tokio::net::UnixListener::bind(&agent_path_) +//! .unwrap(); +//! thrussh_keys::agent::server::serve(tokio_stream::wrappers::UnixListenerStream::new(listener), X {}).await +//! }); +//! let key = decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap(); +//! let public = key.clone_public_key(); +//! core.block_on(async move { +//! let stream = tokio::net::UnixStream::connect(&agent_path).await?; +//! let mut client = agent::client::AgentClient::connect(stream); +//! client.add_identity(&key, &[agent::Constraint::KeyLifetime { seconds: 60 }]).await?; +//! client.request_identities().await?; +//! let buf = b"signed message"; +//! let sig = client.sign_request(&public, cryptovec::CryptoVec::from_slice(&buf[..])).await.1.unwrap(); +//! // Here, `sig` is encoded in a format usable internally by the SSH protocol. +//! Ok::<(), Error>(()) +//! }).unwrap() +//! } +//!``` + +#![recursion_limit = "128"] +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate thiserror; +#[macro_use] +extern crate log; + +#[cfg(test)] +extern crate env_logger; + +use byteorder::{BigEndian, WriteBytesExt}; +use data_encoding::BASE64_MIME; +use std::borrow::Cow; +use std::fs::{File, OpenOptions}; +use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write}; +use std::path::Path; + +pub mod encoding; +pub mod key; +pub mod signature; + +mod format; +pub use format::*; + +/// A module to write SSH agent. +pub mod agent; + +#[derive(Debug, Error)] +pub enum Error { + /// The key could not be read, for an unknown reason + #[error("Could not read key")] + CouldNotReadKey, + /// The type of the key is unsupported + #[error("Unsupported key type")] + UnsupportedKeyType(Vec), + /// The key is encrypted (should supply a password?) + #[error("The key is encrypted")] + KeyIsEncrypted, + /// Home directory could not be found + #[error("No home directory found")] + NoHomeDir, + /// The server key has changed + #[error("The server key changed at line {}", line)] + KeyChanged { line: usize }, + /// The key uses an unsupported algorithm + #[error("Unknown key algorithm")] + UnknownAlgorithm(yasna::models::ObjectIdentifier), + /// Index out of bounds + #[error("Index out of bounds")] + IndexOutOfBounds, + /// Unknown signature type + #[error("Unknown signature type: {}", sig_type)] + UnknownSignatureType { sig_type: String }, + /// Agent protocol error + #[error("Agent protocol error")] + AgentProtocolError, + #[error("Agent failure")] + AgentFailure, + #[error(transparent)] + IO(#[from] std::io::Error), + + #[cfg(feature = "openssl")] + #[error(transparent)] + Openssl(#[from] openssl::error::ErrorStack), + + #[error(transparent)] + BlockMode(#[from] block_modes::BlockModeError), + + #[error("Base64 decoding error: {0}")] + Decode(#[from] data_encoding::DecodeError), + #[error("ASN1 decoding error: {0}")] + ASN1(yasna::ASN1Error), + #[error("Environment variable `{0}` not found")] + EnvVar(&'static str), + #[error("Unable to connect to ssh-agent. The environment variable `SSH_AUTH_SOCK` \ + was set, but it points to a nonexistent file or directory.")] + BadAuthSock, +} + +impl From for Error { + fn from(e: yasna::ASN1Error) -> Error { + Error::ASN1(e) + } +} + +const KEYTYPE_ED25519: &'static [u8] = b"ssh-ed25519"; +const KEYTYPE_RSA: &'static [u8] = b"ssh-rsa"; + +/// Load a public key from a file. Ed25519 and RSA keys are supported. +/// +/// ``` +/// thrussh_keys::load_public_key("/home/pe/.ssh/id_ed25519.pub").unwrap(); +/// ``` +pub fn load_public_key>(path: P) -> Result { + let mut pubkey = String::new(); + let mut file = File::open(path.as_ref())?; + file.read_to_string(&mut pubkey)?; + + let mut split = pubkey.split_whitespace(); + match (split.next(), split.next()) { + (Some(_), Some(key)) => parse_public_key_base64(key), + (Some(key), None) => parse_public_key_base64(key), + _ => Err(Error::CouldNotReadKey.into()), + } +} + +/// Reads a public key from the standard encoding. In some cases, the +/// encoding is prefixed with a key type identifier and a space (such +/// as `ssh-ed25519 AAAAC3N...`). +/// +/// ``` +/// thrussh_keys::parse_public_key_base64("AAAAC3NzaC1lZDI1NTE5AAAAIJdD7y3aLq454yWBdwLWbieU1ebz9/cu7/QEXn9OIeZJ").is_ok(); +/// ``` +pub fn parse_public_key_base64(key: &str) -> Result { + let base = BASE64_MIME.decode(key.as_bytes())?; + Ok(key::parse_public_key(&base)?) +} + +pub trait PublicKeyBase64 { + /// Create the base64 part of the public key blob. + fn public_key_bytes(&self) -> Vec; + fn public_key_base64(&self) -> String { + let mut s = BASE64_MIME.encode(&self.public_key_bytes()); + assert_eq!(s.pop(), Some('\n')); + assert_eq!(s.pop(), Some('\r')); + s + } +} + +impl PublicKeyBase64 for key::PublicKey { + fn public_key_bytes(&self) -> Vec { + let mut s = Vec::new(); + match *self { + key::PublicKey::Ed25519(ref publickey) => { + let name = b"ssh-ed25519"; + s.write_u32::(name.len() as u32).unwrap(); + s.extend_from_slice(name); + s.write_u32::(publickey.key.len() as u32) + .unwrap(); + s.extend_from_slice(&publickey.key); + } + #[cfg(feature = "openssl")] + key::PublicKey::RSA { ref key, .. } => { + use encoding::Encoding; + let name = b"ssh-rsa"; + s.write_u32::(name.len() as u32).unwrap(); + s.extend_from_slice(name); + s.extend_ssh_mpint(&key.0.rsa().unwrap().e().to_vec()); + s.extend_ssh_mpint(&key.0.rsa().unwrap().n().to_vec()); + } + } + s + } +} + +impl PublicKeyBase64 for key::KeyPair { + fn public_key_bytes(&self) -> Vec { + let name = self.name().as_bytes(); + let mut s = Vec::new(); + s.write_u32::(name.len() as u32).unwrap(); + s.extend_from_slice(name); + match *self { + key::KeyPair::Ed25519(ref key) => { + let public = &key.key[32..]; + s.write_u32::(32).unwrap(); + s.extend_from_slice(&public); + } + #[cfg(feature = "openssl")] + key::KeyPair::RSA { ref key, .. } => { + use encoding::Encoding; + s.extend_ssh_mpint(&key.e().to_vec()); + s.extend_ssh_mpint(&key.n().to_vec()); + } + } + s + } +} + +/// Write a public key onto the provided `Write`, encoded in base-64. +pub fn write_public_key_base64( + mut w: W, + publickey: &key::PublicKey, +) -> Result<(), Error> { + let pk = publickey.public_key_base64(); + writeln!(w, "{} {}", publickey.name(), pk)?; + Ok(()) +} + +/// Load a secret key, deciphering it with the supplied password if necessary. +pub fn load_secret_key>( + secret_: P, + password: Option<&str>, +) -> Result { + let mut secret_file = std::fs::File::open(secret_)?; + let mut secret = String::new(); + secret_file.read_to_string(&mut secret)?; + decode_secret_key(&secret, password) +} + +fn is_base64_char(c: char) -> bool { + (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || c == '/' + || c == '+' + || c == '=' +} + +/// Record a host's public key into a nonstandard location. +pub fn learn_known_hosts_path>( + host: &str, + port: u16, + pubkey: &key::PublicKey, + path: P, +) -> Result<(), Error> { + if let Some(parent) = path.as_ref().parent() { + std::fs::create_dir_all(parent)? + } + let mut file = OpenOptions::new() + .read(true) + .append(true) + .create(true) + .open(path)?; + + // Test whether the known_hosts file ends with a \n + let mut buf = [0; 1]; + let mut ends_in_newline = false; + if file.seek(SeekFrom::End(-1)).is_ok() { + file.read_exact(&mut buf)?; + ends_in_newline = buf[0] == b'\n'; + } + + // Write the key. + file.seek(SeekFrom::End(0))?; + let mut file = std::io::BufWriter::new(file); + if !ends_in_newline { + file.write(b"\n")?; + } + if port != 22 { + write!(file, "[{}]:{} ", host, port)? + } else { + write!(file, "{} ", host)? + } + write_public_key_base64(&mut file, pubkey)?; + file.write(b"\n")?; + Ok(()) +} + +/// Check that a server key matches the one recorded in file `path`. +pub fn check_known_hosts_path>( + host: &str, + port: u16, + pubkey: &key::PublicKey, + path: P, +) -> Result { + let mut f = if let Ok(f) = File::open(path) { + BufReader::new(f) + } else { + return Ok(false); + }; + let mut buffer = String::new(); + + let host_port = if port == 22 { + Cow::Borrowed(host) + } else { + Cow::Owned(format!("[{}]:{}", host, port)) + }; + debug!("host_port = {:?}", host_port); + let mut line = 1; + while f.read_line(&mut buffer).unwrap() > 0 { + { + if buffer.as_bytes()[0] == b'#' { + buffer.clear(); + continue; + } + debug!("line = {:?}", buffer); + let mut s = buffer.split(' '); + let hosts = s.next(); + let _ = s.next(); + let key = s.next(); + match (hosts, key) { + (Some(h), Some(k)) => { + debug!("{:?} {:?}", h, k); + let host_matches = h.split(',').any(|x| x == host_port); + if host_matches { + if &parse_public_key_base64(k)? == pubkey { + return Ok(true); + } else { + return Err((Error::KeyChanged { line }).into()); + } + } + } + _ => {} + } + } + buffer.clear(); + line += 1; + } + Ok(false) +} + +/// Record a host's public key into the user's known_hosts file. +#[cfg(target_os = "windows")] +pub fn learn_known_hosts(host: &str, port: u16, pubkey: &key::PublicKey) -> Result<(), Error> { + if let Some(mut known_host_file) = dirs::home_dir() { + known_host_file.push("ssh"); + known_host_file.push("known_hosts"); + learn_known_hosts_path(host, port, pubkey, &known_host_file) + } else { + Err(Error::NoHomeDir) + } +} + +/// Record a host's public key into the user's known_hosts file. +#[cfg(not(target_os = "windows"))] +pub fn learn_known_hosts(host: &str, port: u16, pubkey: &key::PublicKey) -> Result<(), Error> { + if let Some(mut known_host_file) = dirs::home_dir() { + known_host_file.push(".ssh"); + known_host_file.push("known_hosts"); + learn_known_hosts_path(host, port, pubkey, &known_host_file) + } else { + Err(Error::NoHomeDir) + } +} + +/// Check whether the host is known, from its standard location. +#[cfg(target_os = "windows")] +pub fn check_known_hosts(host: &str, port: u16, pubkey: &key::PublicKey) -> Result { + if let Some(mut known_host_file) = dirs::home_dir() { + known_host_file.push("ssh"); + known_host_file.push("known_hosts"); + check_known_hosts_path(host, port, pubkey, &known_host_file) + } else { + Err(Error::NoHomeDir.into()) + } +} + +/// Check whether the host is known, from its standard location. +#[cfg(not(target_os = "windows"))] +pub fn check_known_hosts(host: &str, port: u16, pubkey: &key::PublicKey) -> Result { + if let Some(mut known_host_file) = dirs::home_dir() { + known_host_file.push(".ssh"); + known_host_file.push("known_hosts"); + check_known_hosts_path(host, port, pubkey, &known_host_file) + } else { + Err(Error::NoHomeDir.into()) + } +} + +#[cfg(test)] +mod test { + extern crate tempdir; + use super::*; + #[cfg(feature = "openssl")] + use futures::Future; + use std::fs::File; + use std::io::Write; + + const ED25519_KEY: &'static str = "-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABDLGyfA39 +J2FcJygtYqi5ISAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIN+Wjn4+4Fcvl2Jl +KpggT+wCRxpSvtqqpVrQrKN1/A22AAAAkOHDLnYZvYS6H9Q3S3Nk4ri3R2jAZlQlBbUos5 +FkHpYgNw65KCWCTXtP7ye2czMC3zjn2r98pJLobsLYQgRiHIv/CUdAdsqbvMPECB+wl/UQ +e+JpiSq66Z6GIt0801skPh20jxOO3F52SoX1IeO5D5PXfZrfSZlw6S8c7bwyp2FHxDewRx +7/wNsnDM0T7nLv/Q== +-----END OPENSSH PRIVATE KEY-----"; + + #[cfg(feature = "openssl")] + const RSA_KEY: &'static str = "-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAuSvQ9m76zhRB4m0BUKPf17lwccj7KQ1Qtse63AOqP/VYItqEH8un +rxPogXNBgrcCEm/ccLZZsyE3qgp3DRQkkqvJhZ6O8VBPsXxjZesRCqoFNCczy+Mf0R/Qmv +Rnpu5+4DDLz0p7vrsRZW9ji/c98KzxeUonWgkplQaCBYLN875WdeUYMGtb1MLfNCEj177j +gZl3CzttLRK3su6dckowXcXYv1gPTPZAwJb49J43o1QhV7+1zdwXvuFM6zuYHdu9ZHSKir +6k1dXOET3/U+LWG5ofAo8oxUWv/7vs6h7MeajwkUeIBOWYtD+wGYRvVpxvj7nyOoWtg+jm +0X6ndnsD+QAAA8irV+ZAq1fmQAAAAAdzc2gtcnNhAAABAQC5K9D2bvrOFEHibQFQo9/XuX +BxyPspDVC2x7rcA6o/9Vgi2oQfy6evE+iBc0GCtwISb9xwtlmzITeqCncNFCSSq8mFno7x +UE+xfGNl6xEKqgU0JzPL4x/RH9Ca9Gem7n7gMMvPSnu+uxFlb2OL9z3wrPF5SidaCSmVBo +IFgs3zvlZ15Rgwa1vUwt80ISPXvuOBmXcLO20tErey7p1ySjBdxdi/WA9M9kDAlvj0njej +VCFXv7XN3Be+4UzrO5gd271kdIqKvqTV1c4RPf9T4tYbmh8CjyjFRa//u+zqHsx5qPCRR4 +gE5Zi0P7AZhG9WnG+PufI6ha2D6ObRfqd2ewP5AAAAAwEAAQAAAQAdELqhI/RsSpO45eFR +9hcZtnrm8WQzImrr9dfn1w9vMKSf++rHTuFIQvi48Q10ZiOGH1bbvlPAIVOqdjAPtnyzJR +HhzmyjhjasJlk30zj+kod0kz63HzSMT9EfsYNfmYoCyMYFCKz52EU3xc87Vhi74XmZz0D0 +CgIj6TyZftmzC4YJCiwwU8K+29nxBhcbFRxpgwAksFL6PCSQsPl4y7yvXGcX+7lpZD8547 +v58q3jIkH1g2tBOusIuaiphDDStVJhVdKA55Z0Kju2kvCqsRIlf1efrq43blRgJFFFCxNZ +8Cpolt4lOHhg+o3ucjILlCOgjDV8dB21YLxmgN5q+xFNAAAAgQC1P+eLUkHDFXnleCEVrW +xL/DFxEyneLQz3IawGdw7cyAb7vxsYrGUvbVUFkxeiv397pDHLZ5U+t5cOYDBZ7G43Mt2g +YfWBuRNvYhHA9Sdf38m5qPA6XCvm51f+FxInwd/kwRKH01RHJuRGsl/4Apu4DqVob8y00V +WTYyV6JBNDkQAAAIEA322lj7ZJXfK/oLhMM/RS+DvaMea1g/q43mdRJFQQso4XRCL6IIVn +oZXFeOxrMIRByVZBw+FSeB6OayWcZMySpJQBo70GdJOc3pJb3js0T+P2XA9+/jwXS58K9a ++IkgLkv9XkfxNGNKyPEEzXC8QQzvjs1LbmO59VLko8ypwHq/cAAACBANQqaULI0qdwa0vm +d3Ae1+k3YLZ0kapSQGVIMT2lkrhKV35tj7HIFpUPa4vitHzcUwtjYhqFezVF+JyPbJ/Fsp +XmEc0g1fFnQp5/SkUwoN2zm8Up52GBelkq2Jk57mOMzWO0QzzNuNV/feJk02b2aE8rrAqP +QR+u0AypRPmzHnOPAAAAEXJvb3RAMTQwOTExNTQ5NDBkAQ== +-----END OPENSSH PRIVATE KEY-----"; + + #[test] + fn test_decode_ed25519_secret_key() { + extern crate env_logger; + env_logger::try_init().unwrap_or(()); + decode_secret_key(ED25519_KEY, Some("blabla")).unwrap(); + } + + #[test] + #[cfg(feature = "openssl")] + fn test_decode_rsa_secret_key() { + extern crate env_logger; + env_logger::try_init().unwrap_or(()); + decode_secret_key(RSA_KEY, None).unwrap(); + } + + #[test] + #[cfg(feature = "openssl")] + fn test_fingerprint() { + let key = parse_public_key_base64( + "AAAAC3NzaC1lZDI1NTE5AAAAILagOJFgwaMNhBWQINinKOXmqS4Gh5NgxgriXwdOoINJ", + ) + .unwrap(); + assert_eq!( + key.fingerprint(), + "ldyiXa1JQakitNU5tErauu8DvWQ1dZ7aXu+rm7KQuog" + ); + } + + #[test] + fn test_check_known_hosts() { + env_logger::try_init().unwrap_or(()); + let dir = tempdir::TempDir::new("thrussh").unwrap(); + let path = dir.path().join("known_hosts"); + { + let mut f = File::create(&path).unwrap(); + f.write(b"[localhost]:13265 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJdD7y3aLq454yWBdwLWbieU1ebz9/cu7/QEXn9OIeZJ\n#pijul.org,37.120.161.53 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G2sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X\npijul.org,37.120.161.53 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G1sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X\n").unwrap(); + } + + // Valid key, non-standard port. + let host = "localhost"; + let port = 13265; + let hostkey = parse_public_key_base64( + "AAAAC3NzaC1lZDI1NTE5AAAAIJdD7y3aLq454yWBdwLWbieU1ebz9/cu7/QEXn9OIeZJ", + ) + .unwrap(); + assert!(check_known_hosts_path(host, port, &hostkey, &path).unwrap()); + + // Valid key, several hosts, port 22 + let host = "pijul.org"; + let port = 22; + let hostkey = parse_public_key_base64( + "AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G1sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X", + ) + .unwrap(); + assert!(check_known_hosts_path(host, port, &hostkey, &path).unwrap()); + + // Now with the key in a comment above, check that it's not recognized + let host = "pijul.org"; + let port = 22; + let hostkey = parse_public_key_base64( + "AAAAC3NzaC1lZDI1NTE5AAAAIA6rWI3G2sz07DnfFlrouTcysQlj2P+jpNSOEWD9OJ3X", + ) + .unwrap(); + assert!(check_known_hosts_path(host, port, &hostkey, &path).is_err()); + } + + #[test] + #[cfg(feature = "openssl")] + fn test_srhb() { + env_logger::try_init().unwrap_or(()); + let key = "AAAAB3NzaC1yc2EAAAADAQABAAACAQC0Xtz3tSNgbUQAXem4d+d6hMx7S8Nwm/DOO2AWyWCru+n/+jQ7wz2b5+3oG2+7GbWZNGj8HCc6wJSA3jUsgv1N6PImIWclD14qvoqY3Dea1J0CJgXnnM1xKzBz9C6pDHGvdtySg+yzEO41Xt4u7HFn4Zx5SGuI2NBsF5mtMLZXSi33jCIWVIkrJVd7sZaY8jiqeVZBB/UvkLPWewGVuSXZHT84pNw4+S0Rh6P6zdNutK+JbeuO+5Bav4h9iw4t2sdRkEiWg/AdMoSKmo97Gigq2mKdW12ivnXxz3VfxrCgYJj9WwaUUWSfnAju5SiNly0cTEAN4dJ7yB0mfLKope1kRhPsNaOuUmMUqlu/hBDM/luOCzNjyVJ+0LLB7SV5vOiV7xkVd4KbEGKou8eeCR3yjFazUe/D1pjYPssPL8cJhTSuMc+/UC9zD8yeEZhB9V+vW4NMUR+lh5+XeOzenl65lWYd/nBZXLBbpUMf1AOfbz65xluwCxr2D2lj46iApSIpvE63i3LzFkbGl9GdUiuZJLMFJzOWdhGGc97cB5OVyf8umZLqMHjaImxHEHrnPh1MOVpv87HYJtSBEsN4/omINCMZrk++CRYAIRKRpPKFWV7NQHcvw3m7XLR3KaTYe+0/MINIZwGdou9fLUU3zSd521vDjA/weasH0CyDHq7sZw=="; + + parse_public_key_base64(key).unwrap(); + } + + #[test] + #[cfg(feature = "openssl")] + fn test_nikao() { + env_logger::try_init().unwrap_or(()); + let key = "-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAw/FG8YLVoXhsUVZcWaY7iZekMxQ2TAfSVh0LTnRuzsumeLhb +0fh4scIt4C4MLwpGe/u3vj290C28jLkOtysqnIpB4iBUrFNRmEz2YuvjOzkFE8Ju +0l1VrTZ9APhpLZvzT2N7YmTXcLz1yWopCe4KqTHczEP4lfkothxEoACXMaxezt5o +wIYfagDaaH6jXJgJk1SQ5VYrROVpDjjX8/Zg01H1faFQUikYx0M8EwL1fY5B80Hd +6DYSok8kUZGfkZT8HQ54DBgocjSs449CVqkVoQC1aDB+LZpMWovY15q7hFgfQmYD +qulbZRWDxxogS6ui/zUR2IpX7wpQMKKkBS1qdQIDAQABAoIBAQCodpcCKfS2gSzP +uapowY1KvP/FkskkEU18EDiaWWyzi1AzVn5LRo+udT6wEacUAoebLU5K2BaMF+aW +Lr1CKnDWaeA/JIDoMDJk+TaU0i5pyppc5LwXTXvOEpzi6rCzL/O++88nR4AbQ7sm +Uom6KdksotwtGvttJe0ktaUi058qaoFZbels5Fwk5bM5GHDdV6De8uQjSfYV813P +tM/6A5rRVBjC5uY0ocBHxPXkqAdHfJuVk0uApjLrbm6k0M2dg1X5oyhDOf7ZIzAg +QGPgvtsVZkQlyrD1OoCMPwzgULPXTe8SktaP9EGvKdMf5kQOqUstqfyx+E4OZa0A +T82weLjBAoGBAOUChhaLQShL3Vsml/Nuhhw5LsxU7Li34QWM6P5AH0HMtsSncH8X +ULYcUKGbCmmMkVb7GtsrHa4ozy0fjq0Iq9cgufolytlvC0t1vKRsOY6poC2MQgaZ +bqRa05IKwhZdHTr9SUwB/ngtVNWRzzbFKLkn2W5oCpQGStAKqz3LbKstAoGBANsJ +EyrXPbWbG+QWzerCIi6shQl+vzOd3cxqWyWJVaZglCXtlyySV2eKWRW7TcVvaXQr +Nzm/99GNnux3pUCY6szy+9eevjFLLHbd+knzCZWKTZiWZWr503h/ztfFwrMzhoAh +z4nukD/OETugPvtG01c2sxZb/F8LH9KORznhlSlpAoGBAJnqg1J9j3JU4tZTbwcG +fo5ThHeCkINp2owPc70GPbvMqf4sBzjz46QyDaM//9SGzFwocplhNhaKiQvrzMnR +LSVucnCEm/xdXLr/y6S6tEiFCwnx3aJv1uQRw2bBYkcDmBTAjVXPdUcyOHU+BYXr +Jv6ioMlKlel8/SUsNoFWypeVAoGAXhr3Bjf1xlm+0O9PRyZjQ0RR4DN5eHbB/XpQ +cL8hclsaK3V5tuek79JL1f9kOYhVeVi74G7uzTSYbCY3dJp+ftGCjDAirNEMaIGU +cEMgAgSqs/0h06VESwg2WRQZQ57GkbR1E2DQzuj9FG4TwSe700OoC9o3gqon4PHJ +/j9CM8kCgYEAtPJf3xaeqtbiVVzpPAGcuPyajTzU0QHPrXEl8zr/+iSK4Thc1K+c +b9sblB+ssEUQD5IQkhTWcsXdslINQeL77WhIMZ2vBAH8Hcin4jgcLmwUZfpfnnFs +QaChXiDsryJZwsRnruvMRX9nedtqHrgnIsJLTXjppIhGhq5Kg4RQfOU= +-----END RSA PRIVATE KEY----- +"; + decode_secret_key(key, None).unwrap(); + } + + #[cfg(feature = "openssl")] + pub const PKCS8_RSA: &'static str = "-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwBGetHjW+3bDQpVktdemnk7JXgu1NBWUM+ysifYLDBvJ9ttX +GNZSyQKA4v/dNr0FhAJ8I9BuOTjYCy1YfKylhl5D/DiSSXFPsQzERMmGgAlYvU2U ++FTxpBC11EZg69CPVMKKevfoUD+PZA5zB7Hc1dXFfwqFc5249SdbAwD39VTbrOUI +WECvWZs6/ucQxHHXP2O9qxWqhzb/ddOnqsDHUNoeceiNiCf2anNymovrIMjAqq1R +t2UP3f06/Zt7Jx5AxKqS4seFkaDlMAK8JkEDuMDOdKI36raHkKanfx8CnGMSNjFQ +QtvnpD8VSGkDTJN3Qs14vj2wvS477BQXkBKN1QIDAQABAoIBABb6xLMw9f+2ENyJ +hTggagXsxTjkS7TElCu2OFp1PpMfTAWl7oDBO7xi+UqvdCcVbHCD35hlWpqsC2Ui +8sBP46n040ts9UumK/Ox5FWaiuYMuDpF6vnfJ94KRcb0+KmeFVf9wpW9zWS0hhJh +jC+yfwpyfiOZ/ad8imGCaOguGHyYiiwbRf381T/1FlaOGSae88h+O8SKTG1Oahq4 +0HZ/KBQf9pij0mfVQhYBzsNu2JsHNx9+DwJkrXT7K9SHBpiBAKisTTCnQmS89GtE +6J2+bq96WgugiM7X6OPnmBmE/q1TgV18OhT+rlvvNi5/n8Z1ag5Xlg1Rtq/bxByP +CeIVHsECgYEA9dX+LQdv/Mg/VGIos2LbpJUhJDj0XWnTRq9Kk2tVzr+9aL5VikEb +09UPIEa2ToL6LjlkDOnyqIMd/WY1W0+9Zf1ttg43S/6Rvv1W8YQde0Nc7QTcuZ1K +9jSSP9hzsa3KZtx0fCtvVHm+ac9fP6u80tqumbiD2F0cnCZcSxOb4+UCgYEAyAKJ +70nNKegH4rTCStAqR7WGAsdPE3hBsC814jguplCpb4TwID+U78Xxu0DQF8WtVJ10 +SJuR0R2q4L9uYWpo0MxdawSK5s9Am27MtJL0mkFQX0QiM7hSZ3oqimsdUdXwxCGg +oktxCUUHDIPJNVd4Xjg0JTh4UZT6WK9hl1zLQzECgYEAiZRCFGc2KCzVLF9m0cXA +kGIZUxFAyMqBv+w3+zq1oegyk1z5uE7pyOpS9cg9HME2TAo4UPXYpLAEZ5z8vWZp +45sp/BoGnlQQsudK8gzzBtnTNp5i/MnnetQ/CNYVIVnWjSxRUHBqdMdRZhv0/Uga +e5KA5myZ9MtfSJA7VJTbyHUCgYBCcS13M1IXaMAt3JRqm+pftfqVs7YeJqXTrGs/ +AiDlGQigRk4quFR2rpAV/3rhWsawxDmb4So4iJ16Wb2GWP4G1sz1vyWRdSnmOJGC +LwtYrvfPHegqvEGLpHa7UsgDpol77hvZriwXwzmLO8A8mxkeW5dfAfpeR5o+mcxW +pvnTEQKBgQCKx6Ln0ku6jDyuDzA9xV2/PET5D75X61R2yhdxi8zurY/5Qon3OWzk +jn/nHT3AZghGngOnzyv9wPMKt9BTHyTB6DlB6bRVLDkmNqZh5Wi8U1/IjyNYI0t2 +xV/JrzLAwPoKk3bkqys3bUmgo6DxVC/6RmMwPQ0rmpw78kOgEej90g== +-----END RSA PRIVATE KEY----- +"; + + #[test] + #[cfg(feature = "openssl")] + fn test_loewenheim() { + env_logger::try_init().unwrap_or(()); + let key = "-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,80E4FCAD049EE007CCE1C65D52CDB87A + +ZKBKtex8+DA/d08TTPp4vY8RV+r+1nUC1La+r0dSiXsfunRNDPcYhHbyA/Fdr9kQ ++d1/E3cEb0k2nq7xYyMzy8hpNp/uHu7UfllGdaBusiPjHR+feg6AQfbM0FWpdGzo +9l/Vho5Ocw8abQq1Q9aPW5QQXBURC7HtCQXbpuYjUAQBeea1LzPCw6UIF80GUUkY +1AycXxVfx1AeURAKTZR4hsxC5pqI4yhAvVNXxP+tTTa9NE8lOP0yqVNurfIqyAnp +5ELMwNdHXZyUcT+EH5PsC69ocQgEZqLs0chvke62woMOjeSpsW5cIjGohW9lOD1f +nJkECVZ50kE0SDvcL4Y338tHwMt7wdwdj1dkAWSUjAJT4ShjqV/TzaLAiNAyRxLl +cm3mAccaFIIBZG/bPLGI0B5+mf9VExXGJrbGlvURhtE3nwmjLg1vT8lVfqbyL3a+ +0tFvmDYn71L97t/3hcD2tVnKLv9g8+/OCsUAk3+/0eS7D6GpmlOMRHdLLUHc4SOm +bIDT/dE6MjsCSm7n/JkTb8P+Ta1Hp94dUnX4pfjzZ+O8V1H8wv7QW5KsuJhJ8cn4 +eS3BEgNH1I4FCCjLsZdWve9ehV3/19WXh+BF4WXFq9b3plmfJgTiZslvjy4dgThm +OhEK44+fN1UhzguofxTR4Maz7lcehQxGAxp14hf1EnaAEt3LVjEPEShgK5dx1Ftu +LWFz9nR4vZcMsaiszElrevqMhPQHXY7cnWqBenkMfkdcQDoZjKvV86K98kBIDMu+ +kf855vqRF8b2n/6HPdm3eqFh/F410nSB0bBSglUfyOZH1nS+cs79RQZEF9fNUmpH +EPQtQ/PALohicj9Vh7rRaMKpsORdC8/Ahh20s01xL6siZ334ka3BLYT94UG796/C +4K1S2kPdUP8POJ2HhaK2l6qaG8tcEX7HbwwZeKiEHVNvWuIGQO9TiDONLycp9x4y +kNM3sv2pI7vEhs7d2NapWgNha1RcTSv0CQ6Th/qhGo73LBpVmKwombVImHAyMGAE +aVF32OycVd9c9tDgW5KdhWedbeaxD6qkSs0no71083kYIS7c6iC1R3ZeufEkMhmx +dwrciWTJ+ZAk6rS975onKz6mo/4PytcCY7Df/6xUxHF3iJCnuK8hNpLdJcdOiqEK +zj/d5YGyw3J2r+NrlV1gs3FyvR3eMCWWH2gpIQISBpnEANY40PxA/ogH+nCUvI/O +n8m437ZeLTg6lnPqsE4nlk2hUEwRdy/SVaQURbn7YlcYIt0e81r5sBXb4MXkLrf0 +XRWmpSggdcaaMuXi7nVSdkgCMjGP7epS7HsfP46OrTtJLHn5LxvdOEaW53nPOVQg +/PlVfDbwWl8adE3i3PDQOw9jhYXnYS3sv4R8M8y2GYEXbINrTJyUGrlNggKFS6oh +Hjgt0gsM2N/D8vBrQwnRtyymRnFd4dXFEYKAyt+vk0sa36eLfl0z6bWzIchkJbdu +raMODVc+NiJE0Qe6bwAi4HSpJ0qw2lKwVHYB8cdnNVv13acApod326/9itdbb3lt +KJaj7gc0n6gmKY6r0/Ddufy1JZ6eihBCSJ64RARBXeg2rZpyT+xxhMEZLK5meOeR +-----END RSA PRIVATE KEY----- +"; + let key = decode_secret_key(key, Some("passphrase")).unwrap(); + let public = key.clone_public_key(); + let buf = b"blabla"; + let sig = key.sign_detached(buf).unwrap(); + assert!(public.verify_detached(buf, sig.as_ref())); + } + + #[test] + #[cfg(feature = "openssl")] + fn test_o01eg() { + env_logger::try_init().unwrap_or(()); + + let key = "-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,EA77308AAF46981303D8C44D548D097E + +QR18hXmAgGehm1QMMYGF34PAtBpTj+8/ZPFx2zZxir7pzDpfYoNAIf/fzLsW1ruG +0xo/ZK/T3/TpMgjmLsCR6q+KU4jmCcCqWQIGWYJt9ljFI5y/CXr5uqP3DKcqtdxQ +fbBAfXJ8ITF+Tj0Cljm2S1KYHor+mkil5Lf/ZNiHxcLfoI3xRnpd+2cemN9Ly9eY +HNTbeWbLosfjwdfPJNWFNV5flm/j49klx/UhXhr5HNFNgp/MlTrvkH4rBt4wYPpE +cZBykt4Fo1KGl95pT22inGxQEXVHF1Cfzrf5doYWxjiRTmfhpPSz/Tt0ev3+jIb8 +Htx6N8tNBoVxwCiQb7jj3XNim2OGohIp5vgW9sh6RDfIvr1jphVOgCTFKSo37xk0 +156EoCVo3VcLf+p0/QitbUHR+RGW/PvUJV/wFR5ShYqjI+N2iPhkD24kftJ/MjPt +AAwCm/GYoYjGDhIzQMB+FETZKU5kz23MQtZFbYjzkcI/RE87c4fkToekNCdQrsoZ +wG0Ne2CxrwwEnipHCqT4qY+lZB9EbqQgbWOXJgxA7lfznBFjdSX7uDc/mnIt9Y6B +MZRXH3PTfotHlHMe+Ypt5lfPBi/nruOl5wLo3L4kY5pUyqR0cXKNycIJZb/pJAnE +ryIb59pZP7njvoHzRqnC9dycnTFW3geK5LU+4+JMUS32F636aorunRCl6IBmVQHL +uZ+ue714fn/Sn6H4dw6IH1HMDG1hr8ozP4sNUCiAQ05LsjDMGTdrUsr2iBBpkQhu +VhUDZy9g/5XF1EgiMbZahmqi5WaJ5K75ToINHb7RjOE7MEiuZ+RPpmYLE0HXyn9X +HTx0ZGr022dDI6nkvUm6OvEwLUUmmGKRHKe0y1EdICGNV+HWqnlhGDbLWeMyUcIY +M6Zh9Dw3WXD3kROf5MrJ6n9MDIXx9jy7nmBh7m6zKjBVIw94TE0dsRcWb0O1IoqS +zLQ6ihno+KsQHDyMVLEUz1TuE52rIpBmqexDm3PdDfCgsNdBKP6QSTcoqcfHKeex +K93FWgSlvFFQQAkJumJJ+B7ZWnK+2pdjdtWwTpflAKNqc8t//WmjWZzCtbhTHCXV +1dnMk7azWltBAuXnjW+OqmuAzyh3ayKgqfW66mzSuyQNa1KqFhqpJxOG7IHvxVfQ +kYeSpqODnL87Zd/dU8s0lOxz3/ymtjPMHlOZ/nHNqW90IIeUwWJKJ46Kv6zXqM1t +MeD1lvysBbU9rmcUdop0D3MOgGpKkinR5gy4pUsARBiz4WhIm8muZFIObWes/GDS +zmmkQRO1IcfXKAHbq/OdwbLBm4vM9nk8vPfszoEQCnfOSd7aWrLRjDR+q2RnzNzh +K+fodaJ864JFIfB/A+aVviVWvBSt0eEbEawhTmNPerMrAQ8tRRhmNxqlDP4gOczi +iKUmK5recsXk5us5Ik7peIR/f9GAghpoJkF0HrHio47SfABuK30pzcj62uNWGljS +3d9UQLCepT6RiPFhks/lgimbtSoiJHql1H9Q/3q4MuO2PuG7FXzlTnui3zGw/Vvy +br8gXU8KyiY9sZVbmplRPF+ar462zcI2kt0a18mr0vbrdqp2eMjb37QDbVBJ+rPE +-----END RSA PRIVATE KEY----- +"; + decode_secret_key(key, Some("12345")).unwrap(); + } + #[test] + #[cfg(feature = "openssl")] + fn test_pkcs8() { + env_logger::try_init().unwrap_or(()); + println!("test"); + decode_secret_key(PKCS8_RSA, Some("blabla")).unwrap(); + } + + #[cfg(feature = "openssl")] + const PKCS8_ENCRYPTED: &'static str = "-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQITo1O0b8YrS0CAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBtLH4T1KOfo1GGr7salhR8BIIE +0KN9ednYwcTGSX3hg7fROhTw7JAJ1D4IdT1fsoGeNu2BFuIgF3cthGHe6S5zceI2 +MpkfwvHbsOlDFWMUIAb/VY8/iYxhNmd5J6NStMYRC9NC0fVzOmrJqE1wITqxtORx +IkzqkgFUbaaiFFQPepsh5CvQfAgGEWV329SsTOKIgyTj97RxfZIKA+TR5J5g2dJY +j346SvHhSxJ4Jc0asccgMb0HGh9UUDzDSql0OIdbnZW5KzYJPOx+aDqnpbz7UzY/ +P8N0w/pEiGmkdkNyvGsdttcjFpOWlLnLDhtLx8dDwi/sbEYHtpMzsYC9jPn3hnds +TcotqjoSZ31O6rJD4z18FOQb4iZs3MohwEdDd9XKblTfYKM62aQJWH6cVQcg+1C7 +jX9l2wmyK26Tkkl5Qg/qSfzrCveke5muZgZkFwL0GCcgPJ8RixSB4GOdSMa/hAMU +kvFAtoV2GluIgmSe1pG5cNMhurxM1dPPf4WnD+9hkFFSsMkTAuxDZIdDk3FA8zof +Yhv0ZTfvT6V+vgH3Hv7Tqcxomy5Qr3tj5vvAqqDU6k7fC4FvkxDh2mG5ovWvc4Nb +Xv8sed0LGpYitIOMldu6650LoZAqJVv5N4cAA2Edqldf7S2Iz1QnA/usXkQd4tLa +Z80+sDNv9eCVkfaJ6kOVLk/ghLdXWJYRLenfQZtVUXrPkaPpNXgD0dlaTN8KuvML +Uw/UGa+4ybnPsdVflI0YkJKbxouhp4iB4S5ACAwqHVmsH5GRnujf10qLoS7RjDAl +o/wSHxdT9BECp7TT8ID65u2mlJvH13iJbktPczGXt07nBiBse6OxsClfBtHkRLzE +QF6UMEXsJnIIMRfrZQnduC8FUOkfPOSXc8r9SeZ3GhfbV/DmWZvFPCpjzKYPsM5+ +N8Bw/iZ7NIH4xzNOgwdp5BzjH9hRtCt4sUKVVlWfEDtTnkHNOusQGKu7HkBF87YZ +RN/Nd3gvHob668JOcGchcOzcsqsgzhGMD8+G9T9oZkFCYtwUXQU2XjMN0R4VtQgZ +rAxWyQau9xXMGyDC67gQ5xSn+oqMK0HmoW8jh2LG/cUowHFAkUxdzGadnjGhMOI2 +zwNJPIjF93eDF/+zW5E1l0iGdiYyHkJbWSvcCuvTwma9FIDB45vOh5mSR+YjjSM5 +nq3THSWNi7Cxqz12Q1+i9pz92T2myYKBBtu1WDh+2KOn5DUkfEadY5SsIu/Rb7ub +5FBihk2RN3y/iZk+36I69HgGg1OElYjps3D+A9AjVby10zxxLAz8U28YqJZm4wA/ +T0HLxBiVw+rsHmLP79KvsT2+b4Diqih+VTXouPWC/W+lELYKSlqnJCat77IxgM9e +YIhzD47OgWl33GJ/R10+RDoDvY4koYE+V5NLglEhbwjloo9Ryv5ywBJNS7mfXMsK +/uf+l2AscZTZ1mhtL38efTQCIRjyFHc3V31DI0UdETADi+/Omz+bXu0D5VvX+7c6 +b1iVZKpJw8KUjzeUV8yOZhvGu3LrQbhkTPVYL555iP1KN0Eya88ra+FUKMwLgjYr +JkUx4iad4dTsGPodwEP/Y9oX/Qk3ZQr+REZ8lg6IBoKKqqrQeBJ9gkm1jfKE6Xkc +Cog3JMeTrb3LiPHgN6gU2P30MRp6L1j1J/MtlOAr5rux +-----END ENCRYPTED PRIVATE KEY-----"; + + #[test] + #[cfg(feature = "openssl")] + fn test_gpg() { + env_logger::try_init().unwrap_or(()); + let algo = [115, 115, 104, 45, 114, 115, 97]; + let key = [ + 0, 0, 0, 7, 115, 115, 104, 45, 114, 115, 97, 0, 0, 0, 3, 1, 0, 1, 0, 0, 1, 129, 0, 163, + 72, 59, 242, 4, 248, 139, 217, 57, 126, 18, 195, 170, 3, 94, 154, 9, 150, 89, 171, 236, + 192, 178, 185, 149, 73, 210, 121, 95, 126, 225, 209, 199, 208, 89, 130, 175, 229, 163, + 102, 176, 155, 69, 199, 155, 71, 214, 170, 61, 202, 2, 207, 66, 198, 147, 65, 10, 176, + 20, 105, 197, 133, 101, 126, 193, 252, 245, 254, 182, 14, 250, 118, 113, 18, 220, 38, + 220, 75, 247, 50, 163, 39, 2, 61, 62, 28, 79, 199, 238, 189, 33, 194, 190, 22, 87, 91, + 1, 215, 115, 99, 138, 124, 197, 127, 237, 228, 170, 42, 25, 117, 1, 106, 36, 54, 163, + 163, 207, 129, 133, 133, 28, 185, 170, 217, 12, 37, 113, 181, 182, 180, 178, 23, 198, + 233, 31, 214, 226, 114, 146, 74, 205, 177, 82, 232, 238, 165, 44, 5, 250, 150, 236, 45, + 30, 189, 254, 118, 55, 154, 21, 20, 184, 235, 223, 5, 20, 132, 249, 147, 179, 88, 146, + 6, 100, 229, 200, 221, 157, 135, 203, 57, 204, 43, 27, 58, 85, 54, 219, 138, 18, 37, + 80, 106, 182, 95, 124, 140, 90, 29, 48, 193, 112, 19, 53, 84, 201, 153, 52, 249, 15, + 41, 5, 11, 147, 18, 8, 27, 31, 114, 45, 224, 118, 111, 176, 86, 88, 23, 150, 184, 252, + 128, 52, 228, 90, 30, 34, 135, 234, 123, 28, 239, 90, 202, 239, 188, 175, 8, 141, 80, + 59, 194, 80, 43, 205, 34, 137, 45, 140, 244, 181, 182, 229, 247, 94, 216, 115, 173, + 107, 184, 170, 102, 78, 249, 4, 186, 234, 169, 148, 98, 128, 33, 115, 232, 126, 84, 76, + 222, 145, 90, 58, 1, 4, 163, 243, 93, 215, 154, 205, 152, 178, 109, 241, 197, 82, 148, + 222, 78, 44, 193, 248, 212, 157, 118, 217, 75, 211, 23, 229, 121, 28, 180, 208, 173, + 204, 14, 111, 226, 25, 163, 220, 95, 78, 175, 189, 168, 67, 159, 179, 176, 200, 150, + 202, 248, 174, 109, 25, 89, 176, 220, 226, 208, 187, 84, 169, 157, 14, 88, 217, 221, + 117, 254, 51, 45, 93, 184, 80, 225, 158, 29, 76, 38, 69, 72, 71, 76, 50, 191, 210, 95, + 152, 175, 26, 207, 91, 7, + ]; + debug!("algo = {:?}", std::str::from_utf8(&algo)); + key::PublicKey::parse(&algo, &key).unwrap(); + } + + #[test] + #[cfg(feature = "openssl")] + fn test_pkcs8_encrypted() { + env_logger::try_init().unwrap_or(()); + println!("test"); + decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap(); + } + + fn test_client_agent(key: key::KeyPair) { + env_logger::try_init().unwrap_or(()); + use std::process::{Command, Stdio}; + let dir = tempdir::TempDir::new("thrussh").unwrap(); + let agent_path = dir.path().join("agent"); + let mut agent = Command::new("ssh-agent") + .arg("-a") + .arg(&agent_path) + .arg("-D") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("failed to execute process"); + std::thread::sleep(std::time::Duration::from_millis(10)); + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + let public = key.clone_public_key(); + let stream = tokio::net::UnixStream::connect(&agent_path).await?; + let mut client = agent::client::AgentClient::connect(stream); + client.add_identity(&key, &[]).await?; + client.request_identities().await?; + let buf = cryptovec::CryptoVec::from_slice(b"blabla"); + let len = buf.len(); + let (_, buf) = client.sign_request(&public, buf).await; + let buf = buf?; + let (a, b) = buf.split_at(len); + match key { + key::KeyPair::Ed25519 { .. } => { + let sig = &b[b.len() - 64..]; + assert!(public.verify_detached(a, sig)); + } + _ => {} + } + Ok::<(), Error>(()) + }) + .unwrap(); + agent.kill().unwrap(); + agent.wait().unwrap(); + } + + #[test] + fn test_client_agent_ed25519() { + let key = decode_secret_key(ED25519_KEY, Some("blabla")).unwrap(); + test_client_agent(key) + } + + #[test] + #[cfg(feature = "openssl")] + fn test_client_agent_rsa() { + let key = decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap(); + test_client_agent(key) + } + + #[test] + #[cfg(feature = "openssl")] + fn test_client_agent_openssh_rsa() { + let key = decode_secret_key(RSA_KEY, None).unwrap(); + test_client_agent(key) + } + + #[test] + #[cfg(feature = "openssl")] + fn test_agent() { + env_logger::try_init().unwrap_or(()); + let dir = tempdir::TempDir::new("thrussh").unwrap(); + let agent_path = dir.path().join("agent"); + + let core = tokio::runtime::Runtime::new().unwrap(); + use agent; + + #[derive(Clone)] + struct X {} + impl agent::server::Agent for X { + fn confirm( + self, + _: std::sync::Arc, + ) -> Box + Send + Unpin> { + Box::new(futures::future::ready((self, true))) + } + } + let agent_path_ = agent_path.clone(); + core.spawn(async move { + let mut listener = tokio::net::UnixListener::bind(&agent_path_).unwrap(); + + agent::server::serve( + Incoming { + listener: &mut listener, + }, + X {}, + ) + .await + }); + let key = decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap(); + let public = key.clone_public_key(); + core.block_on(async move { + let stream = tokio::net::UnixStream::connect(&agent_path).await?; + let mut client = agent::client::AgentClient::connect(stream); + client + .add_identity(&key, &[agent::Constraint::KeyLifetime { seconds: 60 }]) + .await?; + client.request_identities().await?; + let buf = cryptovec::CryptoVec::from_slice(b"blabla"); + let len = buf.len(); + let (_, buf) = client.sign_request(&public, buf).await; + let buf = buf?; + let (a, b) = buf.split_at(len); + match key { + key::KeyPair::Ed25519 { .. } => { + let sig = &b[b.len() - 64..]; + assert!(public.verify_detached(a, sig)); + } + _ => {} + } + Ok::<(), Error>(()) + }) + .unwrap() + } + + struct Incoming<'a> { + listener: &'a mut tokio::net::UnixListener, + } + impl futures::stream::Stream for Incoming<'_> { + type Item = Result; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let (sock, _addr) = futures::ready!(self.get_mut().listener.poll_accept(cx))?; + std::task::Poll::Ready(Some(Ok(sock))) + } + } +} diff --git a/thrussh-keys/src/pem.rs b/thrussh-keys/src/pem.rs new file mode 100644 index 00000000..ec9b71b7 --- /dev/null +++ b/thrussh-keys/src/pem.rs @@ -0,0 +1,340 @@ +use {Error, ErrorKind, KEYTYPE_ED25519, KEYTYPE_RSA, rsa_key_from_components}; +use std; +use thrussh::encoding::Reader; +use thrussh::key; +use super::is_base64_char; +use hex::FromHex; +use base64::{decode_config, MIME}; +use ring::signature; +use untrusted; +use yasna; +use ring; + +use openssl::symm::{encrypt, decrypt, Cipher, Mode, Crypter}; +use openssl::hash::{MessageDigest, Hasher}; +use bcrypt_pbkdf; + + +const PBES2: &'static [u64] = &[1, 2, 840, 113549, 1, 5, 13]; +const PBKDF2: &'static [u64] = &[1, 2, 840, 113549, 1, 5, 12]; +const HMAC_SHA256: &'static [u64] = &[1, 2, 840, 113549, 2, 9]; +const AES256CBC: &'static [u64] = &[2, 16, 840, 1, 101, 3, 4, 1, 42]; +const ED25519: &'static [u64] = &[1, 3, 101, 112]; +const RSA: &'static [u64] = &[1, 2, 840, 113549, 1, 1, 1]; + +// https://tools.ietf.org/html/rfc5208 +fn decode_pkcs8( + secret: &[u8], + password: Option<&[u8]>, +) -> Result<(key::Algorithm, super::KeyPairComponents), Error> { + if let Some(pass) = password { + // let mut sec = Vec::new(); + let secret = yasna::parse_der(&secret, |reader| { + reader.read_sequence(|reader| { + // Encryption parameters + let parameters = reader.next().read_sequence(|reader| { + let oid = reader.next().read_oid()?; + debug!("oid = {:?} {:?}", oid, oid.components().as_slice() == PBES2); + if oid.components().as_slice() == PBES2 { + asn1_read_pbes2(reader) + } else { + Ok(Err(ErrorKind::UnknownAlgorithm(oid).into())) + } + })?; + // Ciphertext + let ciphertext = reader.next().read_bytes()?; + Ok(parameters.map(|p| p.decrypt(pass, &ciphertext))) + }) + })???; + debug!("secret {:?}", secret); + + let mut oid = None; + yasna::parse_der(&secret, |reader| { + reader.read_sequence(|reader| { + let version = reader.next().read_u64()?; + debug!("version = {:?}", version); + reader.next().read_sequence(|reader| { + oid = Some(reader.next().read_oid()?); + Ok(()) + }).unwrap_or(()); + Ok(()) + }) + }).unwrap_or(()); + + debug!("pkcs8 oid {:?}", oid); + let oid = if let Some(oid) = oid { + oid + } else { + return Err(ErrorKind::CouldNotReadKey.into()) + }; + if oid.components().as_slice() == ED25519 { + let components = signature::primitive::Ed25519KeyPairComponents::from_pkcs8( + untrusted::Input::from(&secret), + )?; + debug!("components!"); + let keypair = signature::Ed25519KeyPair::from_pkcs8(untrusted::Input::from(&secret))?; + debug!("keypair!"); + Ok((key::Algorithm::Ed25519(keypair), + super::KeyPairComponents::Ed25519(components))) + + } else if oid.components().as_slice() == RSA { + let components = signature::primitive::RSAKeyPairComponents::from_pkcs8( + untrusted::Input::from(&secret), + )?; + + let keypair = signature::RSAKeyPair::from_pkcs8(untrusted::Input::from(&secret))?; + Ok(( + key::Algorithm::RSA( + std::sync::Arc::new(keypair), + key::RSAPublicKey { + n: components.n.as_slice_less_safe().to_vec(), + e: components.e.as_slice_less_safe().to_vec(), + hash: key::SignatureHash::SHA2_512, + }, + ), + super::KeyPairComponents::RSA( + super::RSAKeyPairComponents::from_components(&components), + ), + )) + } else { + Err(ErrorKind::CouldNotReadKey.into()) + } + } else { + Err(ErrorKind::KeyIsEncrypted.into()) + } +} + +#[cfg(test)] +use env_logger; + +#[test] +fn test_read_write_pkcs8() { + env_logger::init().unwrap_or(()); + let r = ring::rand::SystemRandom::new(); + let key = ring::signature::Ed25519KeyPair::generate_pkcs8(&r).unwrap(); + let password = b"blabla"; + let ciphertext = encode_pkcs8(&r, &key, Some(password), 100).unwrap(); + let (_, comp) = decode_pkcs8(&ciphertext, Some(password)).unwrap(); + use super::KeyPairComponents; + match comp { + KeyPairComponents::Ed25519(_) => debug!("Ed25519"), + KeyPairComponents::RSA(_) => debug!("RSA"), + } +} + + +use yasna::models::ObjectIdentifier; +pub fn encode_pkcs8( + rand: &R, + plaintext: &[u8], + password: Option<&[u8]>, + rounds: u32 +) -> Result, Error> { + if let Some(pass) = password { + + let mut salt = [0; 64]; + rand.fill(&mut salt)?; + let mut iv = [0; 16]; + rand.fill(&mut iv)?; + let mut key = [0; 32]; // AES256-CBC + ring::pbkdf2::derive(&ring::digest::SHA256, rounds, &salt, pass, &mut key[..]); + debug!("key = {:?}", key); + + let mut plaintext = plaintext.to_vec(); + let padding_len = 32 - (plaintext.len() % 32); + plaintext.extend(std::iter::repeat(padding_len as u8).take(padding_len)); + + debug!("plaintext {:?}", plaintext); + let ciphertext = encrypt(Cipher::aes_256_cbc(), &key, Some(&iv), &plaintext)?; + + let v = yasna::construct_der(|writer| { + writer.write_sequence(|writer| { + // Encryption parameters + writer.next().write_sequence(|writer| { + writer.next().write_oid(&ObjectIdentifier::from_slice(PBES2)); + asn1_write_pbes2(writer.next(), rounds as u64, &salt, &iv) + }); + // Ciphertext + writer.next().write_bytes(&ciphertext[..]) + }) + }); + Ok(v) + } else { + Err(ErrorKind::KeyIsEncrypted.into()) + } +} + +fn asn1_write_pbes2(writer: yasna::DERWriter, rounds: u64, salt: &[u8], iv: &[u8]) { + writer.write_sequence(|writer| { + // 1. Key generation algorithm + writer.next().write_sequence(|writer| { + writer.next().write_oid(&ObjectIdentifier::from_slice(PBKDF2)); + asn1_write_pbkdf2(writer.next(), rounds, salt) + }); + // 2. Encryption algorithm. + writer.next().write_sequence(|writer| { + writer.next().write_oid(&ObjectIdentifier::from_slice(AES256CBC)); + writer.next().write_bytes(iv) + }); + }) +} + +fn asn1_write_pbkdf2(writer: yasna::DERWriter, rounds: u64, salt: &[u8]) { + writer.write_sequence(|writer| { + writer.next().write_bytes(salt); + writer.next().write_u64(rounds); + writer.next().write_sequence(|writer| { + writer.next().write_oid(&ObjectIdentifier::from_slice(HMAC_SHA256)); + writer.next().write_null() + }) + }) +} + +enum Algorithms { + Pbes2(KeyDerivation, Encryption), +} + +impl Algorithms { + fn decrypt(&self, password: &[u8], cipher: &[u8]) -> Result, Error> { + match *self { + Algorithms::Pbes2(ref der, ref enc) => { + let mut key = enc.key(); + der.derive(password, &mut key); + let out = enc.decrypt(&key, cipher)?; + Ok(out) + } + } + } +} + +impl KeyDerivation { + fn derive(&self, password: &[u8], key: &mut [u8]) { + match *self { + KeyDerivation::Pbkdf2 { + ref salt, + rounds, + digest, + } => ring::pbkdf2::derive(digest, rounds as u32, salt, password, key), + } + } +} + +enum Key { + K128([u8; 16]), + K256([u8; 32]), +} + +impl std::ops::Deref for Key { + type Target = [u8]; + fn deref(&self) -> &[u8] { + match *self { + Key::K128(ref k) => k, + Key::K256(ref k) => k, + } + } +} + +impl std::ops::DerefMut for Key { + fn deref_mut(&mut self) -> &mut [u8] { + match *self { + Key::K128(ref mut k) => k, + Key::K256(ref mut k) => k, + } + } +} + +impl Encryption { + fn key(&self) -> Key { + match *self { + Encryption::Aes128Cbc(_) => Key::K128([0; 16]), + Encryption::Aes256Cbc(_) => Key::K256([0; 32]), + } + } + + fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result, Error> { + let (cipher, iv) = match *self { + Encryption::Aes128Cbc(ref iv) => (Cipher::aes_128_cbc(), iv), + Encryption::Aes256Cbc(ref iv) => (Cipher::aes_256_cbc(), iv), + }; + let mut dec = decrypt( + cipher, + &key, + Some(&iv[..]), + ciphertext + )?; + pkcs_unpad(&mut dec); + Ok(dec) + } +} + +enum KeyDerivation { + Pbkdf2 { + salt: Vec, + rounds: u64, + digest: &'static ring::digest::Algorithm, + }, +} + +fn asn1_read_pbes2( + reader: &mut yasna::BERReaderSeq, +) -> Result, yasna::ASN1Error> { + reader.next().read_sequence(|reader| { + // PBES2 has two components. + // 1. Key generation algorithm + let keygen = reader.next().read_sequence(|reader| { + let oid = reader.next().read_oid()?; + if oid.components().as_slice() == PBKDF2 { + asn1_read_pbkdf2(reader) + } else { + Ok(Err(ErrorKind::UnknownAlgorithm(oid).into())) + } + })?; + // 2. Encryption algorithm. + let algorithm = reader.next().read_sequence(|reader| { + let oid = reader.next().read_oid()?; + if oid.components().as_slice() == AES256CBC { + asn1_read_aes256cbc(reader) + } else { + Ok(Err(ErrorKind::UnknownAlgorithm(oid).into())) + } + })?; + Ok(keygen.and_then(|keygen| { + algorithm.map(|algo| Algorithms::Pbes2(keygen, algo)) + })) + }) +} + +fn asn1_read_pbkdf2( + reader: &mut yasna::BERReaderSeq, +) -> Result, yasna::ASN1Error> { + reader.next().read_sequence(|reader| { + let salt = reader.next().read_bytes()?; + let rounds = reader.next().read_u64()?; + let digest = reader.next().read_sequence(|reader| { + let oid = reader.next().read_oid()?; + if oid.components().as_slice() == HMAC_SHA256 { + reader.next().read_null()?; + Ok(Ok(&ring::digest::SHA256)) + } else { + Ok(Err(ErrorKind::UnknownAlgorithm(oid).into())) + } + })?; + Ok(digest.map(|digest| { + KeyDerivation::Pbkdf2 { + salt, + rounds, + digest, + } + })) + }) +} + +fn asn1_read_aes256cbc( + reader: &mut yasna::BERReaderSeq, +) -> Result, yasna::ASN1Error> { + let iv = reader.next().read_bytes()?; + let mut i = [0; 16]; + i.clone_from_slice(&iv); + Ok(Ok(Encryption::Aes256Cbc(i))) + +} diff --git a/thrussh-keys/src/signature.rs b/thrussh-keys/src/signature.rs new file mode 100644 index 00000000..f0719ef4 --- /dev/null +++ b/thrussh-keys/src/signature.rs @@ -0,0 +1,150 @@ +use crate::key::SignatureHash; +use crate::Error; +use byteorder::{BigEndian, WriteBytesExt}; +use serde; +use serde::de::{SeqAccess, Visitor}; +use serde::ser::SerializeTuple; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; + +pub struct SignatureBytes(pub [u8; 64]); + +/// The type of a signature, depending on the algorithm used. +#[derive(Serialize, Deserialize, Clone)] +pub enum Signature { + /// An Ed25519 signature + Ed25519(SignatureBytes), + /// An RSA signature + RSA { hash: SignatureHash, bytes: Vec }, +} + +impl Signature { + pub fn to_base64(&self) -> String { + use crate::encoding::Encoding; + let mut bytes_ = Vec::new(); + match self { + Signature::Ed25519(ref bytes) => { + let t = b"ssh-ed25519"; + bytes_ + .write_u32::((t.len() + bytes.0.len() + 8) as u32) + .unwrap(); + bytes_.extend_ssh_string(t); + bytes_.extend_ssh_string(&bytes.0[..]); + } + Signature::RSA { + ref hash, + ref bytes, + } => { + let t = match hash { + SignatureHash::SHA2_256 => &b"rsa-sha2-256"[..], + SignatureHash::SHA2_512 => &b"rsa-sha2-512"[..], + SignatureHash::SHA1 => &b"ssh-rsa"[..], + }; + bytes_ + .write_u32::((t.len() + bytes.len() + 8) as u32) + .unwrap(); + bytes_.extend_ssh_string(t); + bytes_.extend_ssh_string(&bytes[..]); + } + } + data_encoding::BASE64_NOPAD.encode(&bytes_[..]) + } + + pub fn from_base64(s: &[u8]) -> Result { + let bytes_ = data_encoding::BASE64_NOPAD.decode(s)?; + use crate::encoding::Reader; + let mut r = bytes_.reader(0); + let sig = r.read_string()?; + let mut r = sig.reader(0); + let typ = r.read_string()?; + let bytes = r.read_string()?; + match typ { + b"ssh-ed25519" => { + let mut bytes_ = [0; 64]; + bytes_.clone_from_slice(bytes); + Ok(Signature::Ed25519(SignatureBytes(bytes_))) + } + b"rsa-sha2-256" => Ok(Signature::RSA { + hash: SignatureHash::SHA2_256, + bytes: bytes.to_vec(), + }), + b"rsa-sha2-512" => Ok(Signature::RSA { + hash: SignatureHash::SHA2_512, + bytes: bytes.to_vec(), + }), + _ => Err((Error::UnknownSignatureType { + sig_type: std::str::from_utf8(typ).unwrap_or("").to_string(), + }) + .into()), + } + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + match *self { + Signature::Ed25519(ref signature) => &signature.0, + Signature::RSA { ref bytes, .. } => &bytes[..], + } + } +} + +impl AsRef<[u8]> for SignatureBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl<'de> Deserialize<'de> for SignatureBytes { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct Vis; + impl<'de> Visitor<'de> for Vis { + type Value = SignatureBytes; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("64 bytes") + } + fn visit_seq>(self, mut seq: A) -> Result { + let mut result = [0; 64]; + for x in result.iter_mut() { + if let Some(y) = seq.next_element()? { + *x = y + } else { + return Err(serde::de::Error::invalid_length(64, &self)); + } + } + Ok(SignatureBytes(result)) + } + } + deserializer.deserialize_tuple(64, Vis) + } +} + +impl Serialize for SignatureBytes { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut tup = serializer.serialize_tuple(64)?; + for byte in self.0.iter() { + tup.serialize_element(byte)?; + } + tup.end() + } +} + +impl fmt::Debug for SignatureBytes { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{:?}", &self.0[..]) + } +} + +impl Clone for SignatureBytes { + fn clone(&self) -> Self { + let mut result = SignatureBytes([0; 64]); + result.0.clone_from_slice(&self.0); + result + } +} diff --git a/thrussh-libsodium/Cargo.toml b/thrussh-libsodium/Cargo.toml new file mode 100644 index 00000000..64abe723 --- /dev/null +++ b/thrussh-libsodium/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "thrussh-libsodium" +version = "0.2.1" +license = "Apache-2.0/MIT" +description = "Straightforward bindings to libsodium" +homepage = "https://nest.pijul.com/pijul/thrussh" +repository = "https://nest.pijul.com/pijul/thrussh" +authors = ["pe@pijul.org "] +include = [ "Cargo.toml", "src/lib.rs" ] +edition = "2018" + +[dependencies] +libc = "0.2" +lazy_static = "1.4" +libsodium-sys = "0.2" + +[build-dependencies] +pkg-config = "0.3" +vcpkg = "0.2" diff --git a/thrussh-libsodium/src/lib.rs b/thrussh-libsodium/src/lib.rs new file mode 100644 index 00000000..3e765aa7 --- /dev/null +++ b/thrussh-libsodium/src/lib.rs @@ -0,0 +1,195 @@ +extern crate libc; +#[macro_use] +extern crate lazy_static; +use libc::c_ulonglong; +use libsodium_sys::*; + +lazy_static! { + static ref SODIUM: i32 = unsafe { sodium_init() }; +} + +pub mod chacha20 { + use super::*; + pub const NONCE_BYTES: usize = 8; + pub const KEY_BYTES: usize = 32; + pub struct Nonce(pub [u8; NONCE_BYTES]); + pub struct Key(pub [u8; KEY_BYTES]); + pub fn chacha20_xor(c: &mut [u8], n: &Nonce, k: &Key) { + lazy_static::initialize(&super::SODIUM); + unsafe { + crypto_stream_chacha20_xor( + c.as_mut_ptr(), + c.as_ptr(), + c.len() as c_ulonglong, + n.0.as_ptr(), + k.0.as_ptr(), + ); + } + } + + pub fn chacha20_xor_ic(c: &mut [u8], n: &Nonce, ic: u64, k: &Key) { + lazy_static::initialize(&super::SODIUM); + unsafe { + crypto_stream_chacha20_xor_ic( + c.as_mut_ptr(), + c.as_ptr(), + c.len() as c_ulonglong, + n.0.as_ptr(), + ic, + k.0.as_ptr(), + ); + } + } +} + +pub mod poly1305 { + use super::*; + pub const KEY_BYTES: usize = 32; + pub const TAG_BYTES: usize = 16; + pub struct Key(pub [u8; KEY_BYTES]); + pub struct Tag(pub [u8; TAG_BYTES]); + pub fn poly1305_auth(m: &[u8], key: &Key) -> Tag { + lazy_static::initialize(&super::SODIUM); + let mut tag = Tag([0; TAG_BYTES]); + unsafe { + crypto_onetimeauth( + tag.0.as_mut_ptr(), + m.as_ptr(), + m.len() as c_ulonglong, + key.0.as_ptr(), + ); + } + tag + } + pub fn poly1305_verify(tag: &[u8], m: &[u8], key: &Key) -> bool { + lazy_static::initialize(&super::SODIUM); + if tag.len() != TAG_BYTES { + false + } else { + unsafe { + crypto_onetimeauth_verify( + tag.as_ptr(), + m.as_ptr(), + m.len() as c_ulonglong, + key.0.as_ptr(), + ) == 0 + } + } + } +} + +pub mod ed25519 { + use super::*; + pub const PUBLICKEY_BYTES: usize = 32; + pub const SECRETKEY_BYTES: usize = 64; + pub const SIGNATURE_BYTES: usize = 64; + + /// Ed25519 public key. + #[derive(Debug, PartialEq, Eq)] + pub struct PublicKey { + /// Actual key + pub key: [u8; PUBLICKEY_BYTES], + } + + impl PublicKey { + pub fn new_zeroed() -> Self { + PublicKey { + key: [0; PUBLICKEY_BYTES], + } + } + } + + /// Ed25519 secret key. + #[derive(Clone)] + pub struct SecretKey { + /// Actual key + pub key: [u8; SECRETKEY_BYTES], + } + + impl SecretKey { + pub fn new_zeroed() -> Self { + SecretKey { + key: [0; SECRETKEY_BYTES], + } + } + } + + pub struct Signature(pub [u8; SIGNATURE_BYTES]); + + /// Generate a key pair. + pub fn keypair() -> (PublicKey, SecretKey) { + unsafe { + lazy_static::initialize(&super::SODIUM); + let mut pk = PublicKey { + key: [0; PUBLICKEY_BYTES], + }; + let mut sk = SecretKey { + key: [0; SECRETKEY_BYTES], + }; + crypto_sign_keypair(pk.key.as_mut_ptr(), sk.key.as_mut_ptr()); + (pk, sk) + } + } + + /// Verify a signature, `sig` could as well be a `Signature`. + pub fn verify_detached(sig: &[u8], m: &[u8], pk: &PublicKey) -> bool { + lazy_static::initialize(&super::SODIUM); + if sig.len() == SIGNATURE_BYTES { + unsafe { + crypto_sign_verify_detached( + sig.as_ptr(), + m.as_ptr(), + m.len() as c_ulonglong, + pk.key.as_ptr(), + ) == 0 + } + } else { + false + } + } + + /// Sign a message with a secret key. + pub fn sign_detached(m: &[u8], sk: &SecretKey) -> Signature { + lazy_static::initialize(&super::SODIUM); + let mut sig = Signature([0; SIGNATURE_BYTES]); + let mut sig_len = 0; + unsafe { + crypto_sign_detached( + sig.0.as_mut_ptr(), + &mut sig_len, + m.as_ptr(), + m.len() as c_ulonglong, + sk.key.as_ptr(), + ); + } + sig + } +} + +pub mod scalarmult { + use super::*; + pub const BYTES: usize = 32; + + #[derive(Debug)] + pub struct Scalar(pub [u8; BYTES]); + #[derive(Debug)] + pub struct GroupElement(pub [u8; BYTES]); + + pub fn scalarmult_base(n: &Scalar) -> GroupElement { + lazy_static::initialize(&super::SODIUM); + let mut q = GroupElement([0; BYTES]); + unsafe { + crypto_scalarmult_curve25519_base(q.0.as_mut_ptr(), n.0.as_ptr()); + } + q + } + + pub fn scalarmult(n: &Scalar, p: &GroupElement) -> GroupElement { + lazy_static::initialize(&super::SODIUM); + let mut q = GroupElement([0; BYTES]); + unsafe { + crypto_scalarmult_curve25519(q.0.as_mut_ptr(), n.0.as_ptr(), p.0.as_ptr()); + } + q + } +} diff --git a/thrussh/Cargo.toml b/thrussh/Cargo.toml new file mode 100755 index 00000000..ff52439d --- /dev/null +++ b/thrussh/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "thrussh" +description = "A client and server SSH library." +keywords = ["ssh"] +version = "0.33.5" +authors = ["Pierre-Étienne Meunier "] +repository = "https://nest.pijul.com/pijul/thrussh" +homepage = "https://pijul.org/thrussh" +documentation = "https://docs.rs/thrussh" +license = "Apache-2.0" +readme = "../README.md" +include = [ +"Cargo.toml", +"src/auth.rs", +"src/compression.rs", +"src/kex.rs", +"src/key.rs", +"src/lib.rs", +"src/msg.rs", +"src/negotiation.rs", +"src/pty.rs", +"src/session.rs", +"src/sshbuffer.rs", +"src/ssh_read.rs", +"src/cipher/chacha20poly1305.rs", +"src/cipher/clear.rs", +"src/cipher/mod.rs", +"src/client/mod.rs", +"src/client/session.rs", +"src/client/encrypted.rs", +"src/client/kex.rs", +"src/client/proxy.rs", +"src/server/mod.rs", +"src/server/encrypted.rs", +"src/server/kex.rs", +"src/server/session.rs", +"src/sodium.rs", +] +edition = "2018" + +[features] +default = [ "flate2" ] + +[dependencies] +byteorder = "1.3" +bitflags = "1.2" +log = "0.4" +thrussh-keys = { version = "0.21.0", path = "../thrussh-keys" } +openssl = { version = "0.10", optional = true } +thrussh-libsodium = "0.2" +cryptovec = "0.6.0" +tokio = { version = "1.0", features = [ "io-util", "rt-multi-thread", "time", "net", "sync", "macros", "process" ] } +futures = "0.3" +thiserror = "1.0" +flate2 = { version = "1.0", optional = true } +rand = "0.8" +sha2 = "0.9" +generic-array = "0.14" +digest = "0.9" + +[dev-dependencies] +env_logger = "0.7" +tokio = { version = "1.0", features = [ "io-util", "rt-multi-thread", "time", "net", "sync", "macros" ] } +anyhow = "1.0" diff --git a/thrussh/examples/client.rs b/thrussh/examples/client.rs new file mode 100644 index 00000000..7350a7be --- /dev/null +++ b/thrussh/examples/client.rs @@ -0,0 +1,77 @@ +extern crate env_logger; +extern crate futures; +extern crate thrussh; +extern crate thrussh_keys; +extern crate tokio; +use anyhow::Context; +use std::sync::Arc; +use thrussh::*; +use thrussh_keys::*; + +struct Client {} + +impl client::Handler for Client { + type Error = thrussh::Error; + type FutureUnit = futures::future::Ready>; + type FutureBool = futures::future::Ready>; + + fn finished_bool(self, b: bool) -> Self::FutureBool { + futures::future::ready(Ok((self, b))) + } + fn finished(self, session: client::Session) -> Self::FutureUnit { + futures::future::ready(Ok((self, session))) + } + fn check_server_key(self, server_public_key: &key::PublicKey) -> Self::FutureBool { + println!("check_server_key: {:?}", server_public_key); + self.finished_bool(true) + } +} + +#[tokio::main] +async fn main() { + env_logger::init(); + let config = thrussh::client::Config::default(); + let config = Arc::new(config); + let sh = Client {}; + + let mut agent = thrussh_keys::agent::client::AgentClient::connect_env() + .await + .unwrap(); + let mut identities = agent.request_identities().await.unwrap(); + let mut session = thrussh::client::connect(config, "127.0.0.1:2200", sh) + .await + .unwrap(); + let (_, auth_res) = session + .authenticate_future("pe", identities.pop().unwrap(), agent) + .await; + let auth_res = auth_res.unwrap(); + println!("=== auth: {}", auth_res); + let mut channel = session + .channel_open_direct_tcpip("localhost", 8000, "localhost", 3333) + .await + .unwrap(); + // let mut channel = session.channel_open_session().await.unwrap(); + println!("=== after open channel"); + let data = b"GET /les_affames.mkv HTTP/1.1\nUser-Agent: curl/7.68.0\nAccept: */*\nConnection: close\n\n"; + channel.data(&data[..]).await.unwrap(); + let mut f = std::fs::File::create("les_affames.mkv").unwrap(); + while let Some(msg) = channel.wait().await { + use std::io::Write; + match msg { + thrussh::ChannelMsg::Data { ref data } => { + f.write_all(&data).unwrap(); + } + thrussh::ChannelMsg::Eof => { + f.flush().unwrap(); + break; + } + _ => {} + } + } + session + .disconnect(Disconnect::ByApplication, "", "English") + .await + .unwrap(); + let res = session.await.context("session await"); + println!("{:#?}", res); +} diff --git a/thrussh/examples/remote_shell_call.rs b/thrussh/examples/remote_shell_call.rs new file mode 100644 index 00000000..b7abebf1 --- /dev/null +++ b/thrussh/examples/remote_shell_call.rs @@ -0,0 +1,103 @@ +use anyhow::Result; +use std::io::Write; +use std::sync::Arc; +use thrussh::*; +use thrussh_keys::*; + +#[tokio::main] +async fn main() -> Result<()> { + let pem = std::fs::read("./my-aws-key.pem")?; + let mut ssh = Session::connect(&pem, "ubuntu", "35.158.158.35:22").await?; + let r = ssh.call("whoami").await?; + assert!(r.success()); + assert_eq!(r.output(), "ubuntu\n"); + ssh.close().await?; + Ok(()) +} + +struct Client {} + +impl client::Handler for Client { + type Error = thrussh::Error; + type FutureUnit = futures::future::Ready>; + type FutureBool = futures::future::Ready>; + + fn finished_bool(self, b: bool) -> Self::FutureBool { + futures::future::ready(Ok((self, b))) + } + fn finished(self, session: client::Session) -> Self::FutureUnit { + futures::future::ready(Ok((self, session))) + } + fn check_server_key(self, _server_public_key: &key::PublicKey) -> Self::FutureBool { + self.finished_bool(true) + } +} + +pub struct Session { + session: client::Handle, +} + +impl Session { + async fn connect( + pem: &[u8], + user: impl Into, + addr: impl std::net::ToSocketAddrs, + ) -> Result { + let key_pair = key::KeyPair::RSA { + key: openssl::rsa::Rsa::private_key_from_pem(pem)?, + hash: key::SignatureHash::SHA2_512, + }; + let config = client::Config::default(); + let config = Arc::new(config); + let sh = Client {}; + let mut agent = agent::client::AgentClient::connect_env().await?; + agent.add_identity(&key_pair, &[]).await?; + let mut identities = agent.request_identities().await?; + let mut session = client::connect(config, addr, sh).await?; + let pubkey = identities.pop().unwrap(); + let (_, auth_res) = session.authenticate_future(user, pubkey, agent).await; + let _auth_res = auth_res?; + Ok(Self { session }) + } + + async fn call(&mut self, command: &str) -> Result { + let mut channel = self.session.channel_open_session().await?; + channel.exec(true, command).await?; + let mut output = Vec::new(); + let mut code = None; + while let Some(msg) = channel.wait().await { + match msg { + thrussh::ChannelMsg::Data { ref data } => { + output.write_all(&data).unwrap(); + } + thrussh::ChannelMsg::ExitStatus { exit_status } => { + code = Some(exit_status); + } + _ => {} + } + } + Ok(CommandResult { output, code }) + } + + async fn close(&mut self) -> Result<()> { + self.session + .disconnect(Disconnect::ByApplication, "", "English") + .await?; + Ok(()) + } +} + +struct CommandResult { + output: Vec, + code: Option, +} + +impl CommandResult { + fn output(&self) -> String { + String::from_utf8_lossy(&self.output).into() + } + + fn success(&self) -> bool { + self.code == Some(0) + } +} diff --git a/thrussh/src/auth.rs b/thrussh/src/auth.rs new file mode 100644 index 00000000..06136987 --- /dev/null +++ b/thrussh/src/auth.rs @@ -0,0 +1,149 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use cryptovec::CryptoVec; +use std::sync::Arc; +use thrussh_keys::encoding; +use thrussh_keys::key; +use tokio::io::{AsyncRead, AsyncWrite}; + +bitflags! { + /// Set of methods, represented by bit flags. + pub struct MethodSet: u32 { + /// The SSH `none` method (no authentication). + const NONE = 1; + /// The SSH `password` method (plaintext passwords). + const PASSWORD = 2; + /// The SSH `publickey` method (sign a challenge sent by the + /// server). + const PUBLICKEY = 4; + /// The SSH `hostbased` method (certain hostnames are allowed + /// by the server). + const HOSTBASED = 8; + /// The SSH `keyboard-interactive` method (answer to a + /// challenge, where the "challenge" can be a password prompt, + /// a bytestring to sign with a smartcard, or something else). + const KEYBOARD_INTERACTIVE = 16; + } +} + +macro_rules! iter { + ( $y:expr, $x:expr ) => {{ + if $y.contains($x) { + $y.remove($x); + return Some($x); + } + }}; +} + +impl Iterator for MethodSet { + type Item = MethodSet; + fn next(&mut self) -> Option { + iter!(self, MethodSet::NONE); + iter!(self, MethodSet::PASSWORD); + iter!(self, MethodSet::PUBLICKEY); + iter!(self, MethodSet::HOSTBASED); + iter!(self, MethodSet::KEYBOARD_INTERACTIVE); + None + } +} + +pub trait Signer: Sized { + type Error: From; + type Future: futures::Future)> + Send; + + fn auth_publickey_sign(self, key: &key::PublicKey, to_sign: CryptoVec) -> Self::Future; +} + +#[derive(Debug, Error)] +pub enum AgentAuthError { + #[error(transparent)] + Send(#[from] crate::SendError), + #[error(transparent)] + Key(#[from] thrussh_keys::Error), +} + +impl Signer + for thrussh_keys::agent::client::AgentClient +{ + type Error = AgentAuthError; + type Future = std::pin::Pin< + Box)> + Send>, + >; + fn auth_publickey_sign(self, key: &key::PublicKey, to_sign: CryptoVec) -> Self::Future { + let fut = self.sign_request(key, to_sign); + futures::FutureExt::boxed(async move { + let (a, b) = fut.await; + (a, b.map_err(AgentAuthError::Key)) + }) + } +} + +#[derive(Debug)] +pub enum Method { + // None, + Password { password: String }, + PublicKey { key: Arc }, + FuturePublicKey { key: key::PublicKey }, + // Hostbased, +} + +impl encoding::Bytes for MethodSet { + fn bytes(&self) -> &'static [u8] { + match *self { + MethodSet::NONE => b"none", + MethodSet::PASSWORD => b"password", + MethodSet::PUBLICKEY => b"publickey", + MethodSet::HOSTBASED => b"hostbased", + MethodSet::KEYBOARD_INTERACTIVE => b"keyboard-interactive", + _ => b"", + } + } +} + +impl MethodSet { + pub(crate) fn from_bytes(b: &[u8]) -> Option { + match b { + b"none" => Some(MethodSet::NONE), + b"password" => Some(MethodSet::PASSWORD), + b"publickey" => Some(MethodSet::PUBLICKEY), + b"hostbased" => Some(MethodSet::HOSTBASED), + b"keyboard-interactive" => Some(MethodSet::KEYBOARD_INTERACTIVE), + _ => None, + } + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct AuthRequest { + pub methods: MethodSet, + pub partial_success: bool, + pub current: Option, + pub rejection_count: usize, +} + +#[doc(hidden)] +#[derive(Debug)] +pub enum CurrentRequest { + PublicKey { + key: CryptoVec, + algo: CryptoVec, + sent_pk_ok: bool, + }, + KeyboardInteractive { + submethods: String, + }, +} diff --git a/thrussh/src/cipher/chacha20poly1305.rs b/thrussh/src/cipher/chacha20poly1305.rs new file mode 100644 index 00000000..79776f7e --- /dev/null +++ b/thrussh/src/cipher/chacha20poly1305.rs @@ -0,0 +1,152 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD + +use super::super::Error; +use byteorder::{BigEndian, ByteOrder}; +use sodium::chacha20::*; + +pub struct OpeningKey { + k1: Key, + k2: Key, +} +pub struct SealingKey { + k1: Key, + k2: Key, +} + +const TAG_LEN: usize = 16; + +pub static CIPHER: super::Cipher = super::Cipher { + name: NAME, + key_len: 64, + make_sealing_cipher, + make_opening_cipher, +}; + +pub const NAME: super::Name = super::Name("chacha20-poly1305@openssh.com"); + +fn make_sealing_cipher(k: &[u8]) -> super::SealingCipher { + let mut k1 = Key([0; KEY_BYTES]); + let mut k2 = Key([0; KEY_BYTES]); + k1.0.clone_from_slice(&k[KEY_BYTES..]); + k2.0.clone_from_slice(&k[..KEY_BYTES]); + super::SealingCipher::Chacha20Poly1305(SealingKey { k1, k2 }) +} + +fn make_opening_cipher(k: &[u8]) -> super::OpeningCipher { + let mut k1 = Key([0; KEY_BYTES]); + let mut k2 = Key([0; KEY_BYTES]); + k1.0.clone_from_slice(&k[KEY_BYTES..]); + k2.0.clone_from_slice(&k[..KEY_BYTES]); + super::OpeningCipher::Chacha20Poly1305(OpeningKey { k1, k2 }) +} + +fn make_counter(sequence_number: u32) -> Nonce { + let mut nonce = Nonce([0; NONCE_BYTES]); + let i0 = NONCE_BYTES - 4; + BigEndian::write_u32(&mut nonce.0[i0..], sequence_number); + nonce +} + +impl super::OpeningKey for OpeningKey { + fn decrypt_packet_length( + &self, + sequence_number: u32, + mut encrypted_packet_length: [u8; 4], + ) -> [u8; 4] { + let nonce = make_counter(sequence_number); + chacha20_xor(&mut encrypted_packet_length, &nonce, &self.k1); + encrypted_packet_length + } + + fn tag_len(&self) -> usize { + TAG_LEN + } + + fn open<'a>( + &self, + sequence_number: u32, + ciphertext_in_plaintext_out: &'a mut [u8], + tag: &[u8], + ) -> Result<&'a [u8], Error> { + let nonce = make_counter(sequence_number); + { + use sodium::poly1305::*; + let mut poly_key = Key([0; 32]); + chacha20_xor(&mut poly_key.0, &nonce, &self.k2); + // let mut tag_ = Tag([0; 16]); + // tag_.0.clone_from_slice(tag); + if !poly1305_verify(&tag, ciphertext_in_plaintext_out, &poly_key) { + return Err(Error::PacketAuth); + } + } + chacha20_xor_ic(&mut ciphertext_in_plaintext_out[4..], &nonce, 1, &self.k2); + Ok(&ciphertext_in_plaintext_out[4..]) + } +} + +impl super::SealingKey for SealingKey { + fn padding_length(&self, payload: &[u8]) -> usize { + let block_size = 8; + let extra_len = super::PACKET_LENGTH_LEN + super::PADDING_LENGTH_LEN; + let padding_len = if payload.len() + extra_len <= super::MINIMUM_PACKET_LEN { + super::MINIMUM_PACKET_LEN - payload.len() - super::PADDING_LENGTH_LEN + } else { + block_size - ((super::PADDING_LENGTH_LEN + payload.len()) % block_size) + }; + if padding_len < super::PACKET_LENGTH_LEN { + padding_len + block_size + } else { + padding_len + } + } + + // As explained in "SSH via CTR mode with stateful decryption" in + // https://openvpn.net/papers/ssh-security.pdf, the padding doesn't need to + // be random because we're doing stateful counter-mode encryption. Use + // fixed padding to avoid PRNG overhead. + fn fill_padding(&self, padding_out: &mut [u8]) { + for padding_byte in padding_out { + *padding_byte = 0; + } + } + + fn tag_len(&self) -> usize { + TAG_LEN + } + + /// Append an encrypted packet with contents `packet_content` at the end of `buffer`. + fn seal( + &self, + sequence_number: u32, + plaintext_in_ciphertext_out: &mut [u8], + tag_out: &mut [u8], + ) { + let mut nonce = make_counter(sequence_number); + { + let (a, b) = plaintext_in_ciphertext_out.split_at_mut(4); + chacha20_xor(a, &nonce, &self.k1); + chacha20_xor_ic(b, &nonce, 1, &self.k2); + } + nonce.0[0] = 0; + use sodium::poly1305::*; + let mut poly_key = Key([0; 32]); + chacha20_xor(&mut poly_key.0, &nonce, &self.k2); + let tag = poly1305_auth(plaintext_in_ciphertext_out, &poly_key); + tag_out.clone_from_slice(&tag.0); + } +} diff --git a/thrussh/src/cipher/clear.rs b/thrussh/src/cipher/clear.rs new file mode 100644 index 00000000..5b95cdb9 --- /dev/null +++ b/thrussh/src/cipher/clear.rs @@ -0,0 +1,70 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use crate::Error; + +#[derive(Debug)] +pub struct Key; + +impl super::OpeningKey for Key { + fn decrypt_packet_length(&self, _seqn: u32, packet_length: [u8; 4]) -> [u8; 4] { + packet_length + } + + fn tag_len(&self) -> usize { + 0 + } + + fn open<'a>( + &self, + _seqn: u32, + ciphertext_in_plaintext_out: &'a mut [u8], + tag: &[u8], + ) -> Result<&'a [u8], Error> { + debug_assert_eq!(tag.len(), 0); // self.tag_len()); + Ok(&ciphertext_in_plaintext_out[4..]) + } +} + +impl super::SealingKey for Key { + // Cleartext packets (including lengths) must be multiple of 8 in + // length. + fn padding_length(&self, payload: &[u8]) -> usize { + let block_size = 8; + let padding_len = block_size - ((5 + payload.len()) % block_size); + if padding_len < 4 { + padding_len + block_size + } else { + padding_len + } + } + + fn fill_padding(&self, padding_out: &mut [u8]) { + // Since the packet is unencrypted anyway, there's no advantage to + // randomizing the padding, so avoid possibly leaking extra RNG state + // by padding with zeros. + for padding_byte in padding_out { + *padding_byte = 0; + } + } + + fn tag_len(&self) -> usize { + 0 + } + + fn seal(&self, _seqn: u32, _plaintext_in_ciphertext_out: &mut [u8], tag_out: &mut [u8]) { + debug_assert_eq!(tag_out.len(), self.tag_len()); + } +} diff --git a/thrussh/src/cipher/mod.rs b/thrussh/src/cipher/mod.rs new file mode 100644 index 00000000..a7939162 --- /dev/null +++ b/thrussh/src/cipher/mod.rs @@ -0,0 +1,195 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +use crate::sshbuffer::SSHBuffer; +use crate::Error; +use byteorder::{BigEndian, ByteOrder}; +use std::num::Wrapping; +use tokio::io::{AsyncRead, AsyncReadExt}; +pub mod chacha20poly1305; +pub mod clear; + +pub struct Cipher { + pub name: Name, + pub key_len: usize, + pub make_opening_cipher: fn(key: &[u8]) -> OpeningCipher, + pub make_sealing_cipher: fn(key: &[u8]) -> SealingCipher, +} + +pub enum OpeningCipher { + Clear(clear::Key), + Chacha20Poly1305(chacha20poly1305::OpeningKey), +} + +impl<'a> OpeningCipher { + fn as_opening_key(&self) -> &dyn OpeningKey { + match *self { + OpeningCipher::Clear(ref key) => key, + OpeningCipher::Chacha20Poly1305(ref key) => key, + } + } +} + +pub enum SealingCipher { + Clear(clear::Key), + Chacha20Poly1305(chacha20poly1305::SealingKey), +} + +impl<'a> SealingCipher { + fn as_sealing_key(&'a self) -> &'a dyn SealingKey { + match *self { + SealingCipher::Clear(ref key) => key, + SealingCipher::Chacha20Poly1305(ref key) => key, + } + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct Name(&'static str); +impl AsRef for Name { + fn as_ref(&self) -> &str { + self.0 + } +} + +pub struct CipherPair { + pub local_to_remote: SealingCipher, + pub remote_to_local: OpeningCipher, +} + +impl std::fmt::Debug for CipherPair { + fn fmt(&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + Ok(()) + } +} + +pub const CLEAR_PAIR: CipherPair = CipherPair { + local_to_remote: SealingCipher::Clear(clear::Key), + remote_to_local: OpeningCipher::Clear(clear::Key), +}; + +pub trait OpeningKey { + fn decrypt_packet_length(&self, seqn: u32, encrypted_packet_length: [u8; 4]) -> [u8; 4]; + + fn tag_len(&self) -> usize; + + fn open<'a>( + &self, + seqn: u32, + ciphertext_in_plaintext_out: &'a mut [u8], + tag: &[u8], + ) -> Result<&'a [u8], Error>; +} + +pub trait SealingKey { + fn padding_length(&self, plaintext: &[u8]) -> usize; + + fn fill_padding(&self, padding_out: &mut [u8]); + + fn tag_len(&self) -> usize; + + fn seal(&self, seqn: u32, plaintext_in_ciphertext_out: &mut [u8], tag_out: &mut [u8]); +} + +pub async fn read<'a, R: AsyncRead + Unpin>( + stream: &'a mut R, + buffer: &'a mut SSHBuffer, + pair: &'a CipherPair, +) -> Result { + if buffer.len == 0 { + let mut len = [0; 4]; + stream.read_exact(&mut len).await?; + debug!("reading, len = {:?}", len); + { + let key = pair.remote_to_local.as_opening_key(); + let seqn = buffer.seqn.0; + buffer.buffer.clear(); + buffer.buffer.extend(&len); + debug!("reading, seqn = {:?}", seqn); + let len = key.decrypt_packet_length(seqn, len); + buffer.len = BigEndian::read_u32(&len) as usize + key.tag_len(); + debug!("reading, clear len = {:?}", buffer.len); + } + } + buffer.buffer.resize(buffer.len + 4); + debug!("read_exact {:?}", buffer.len + 4); + stream.read_exact(&mut buffer.buffer[4..]).await?; + debug!("read_exact done"); + let key = pair.remote_to_local.as_opening_key(); + let seqn = buffer.seqn.0; + let ciphertext_len = buffer.buffer.len() - key.tag_len(); + let (ciphertext, tag) = buffer.buffer.split_at_mut(ciphertext_len); + let plaintext = key.open(seqn, ciphertext, tag)?; + + let padding_length = plaintext[0] as usize; + debug!("reading, padding_length {:?}", padding_length); + let plaintext_end = plaintext + .len() + .checked_sub(padding_length) + .ok_or(Error::IndexOutOfBounds)?; + + // Sequence numbers are on 32 bits and wrap. + // https://tools.ietf.org/html/rfc4253#section-6.4 + buffer.seqn += Wrapping(1); + buffer.len = 0; + + // Remove the padding + buffer.buffer.resize(plaintext_end + 4); + + Ok(plaintext_end + 4) +} + +impl CipherPair { + pub fn write(&self, payload: &[u8], buffer: &mut SSHBuffer) { + // https://tools.ietf.org/html/rfc4253#section-6 + // + // The variables `payload`, `packet_length` and `padding_length` refer + // to the protocol fields of the same names. + debug!("writing, seqn = {:?}", buffer.seqn.0); + let key = self.local_to_remote.as_sealing_key(); + + let padding_length = key.padding_length(payload); + debug!("padding length {:?}", padding_length); + let packet_length = PADDING_LENGTH_LEN + payload.len() + padding_length; + debug!("packet_length {:?}", packet_length); + let offset = buffer.buffer.len(); + + // Maximum packet length: + // https://tools.ietf.org/html/rfc4253#section-6.1 + assert!(packet_length <= std::u32::MAX as usize); + buffer.buffer.push_u32_be(packet_length as u32); + + assert!(padding_length <= std::u8::MAX as usize); + buffer.buffer.push(padding_length as u8); + buffer.buffer.extend(payload); + key.fill_padding(buffer.buffer.resize_mut(padding_length)); + buffer.buffer.resize_mut(key.tag_len()); + + let (plaintext, tag) = + buffer.buffer[offset..].split_at_mut(PACKET_LENGTH_LEN + packet_length); + + key.seal(buffer.seqn.0, plaintext, tag); + + buffer.bytes += payload.len(); + // Sequence numbers are on 32 bits and wrap. + // https://tools.ietf.org/html/rfc4253#section-6.4 + buffer.seqn += Wrapping(1); + } +} + +pub const PACKET_LENGTH_LEN: usize = 4; + +const MINIMUM_PACKET_LEN: usize = 16; + +const PADDING_LENGTH_LEN: usize = 1; diff --git a/thrussh/src/client/encrypted.rs b/thrussh/src/client/encrypted.rs new file mode 100644 index 00000000..ca24c6d1 --- /dev/null +++ b/thrussh/src/client/encrypted.rs @@ -0,0 +1,635 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +use super::{Msg, Reply}; +use crate::auth; +use crate::key::PubKey; +use crate::msg; +use crate::negotiation; +use crate::negotiation::Named; +use crate::negotiation::Select; +use crate::session::*; +use crate::{ChannelId, ChannelOpenFailure, Error, Sig}; +use cryptovec::CryptoVec; +use std::cell::RefCell; +use thrussh_keys::encoding::{Encoding, Reader}; + +thread_local! { + static SIGNATURE_BUFFER: RefCell = RefCell::new(CryptoVec::new()); +} + +impl super::Session { + pub(crate) async fn client_read_encrypted( + mut self, + client: &mut Option, + buf: &[u8], + ) -> Result { + debug!( + "client_read_encrypted, buf = {:?}", + &buf[..buf.len().min(20)] + ); + // Either this packet is a KEXINIT, in which case we start a key re-exchange. + if buf[0] == msg::KEXINIT { + // Now, if we're encrypted: + if let Some(ref mut enc) = self.common.encrypted { + // If we're not currently rekeying, but buf is a rekey request + if let Some(Kex::KexInit(kexinit)) = enc.rekey.take() { + enc.rekey = Some(Kex::KexDhDone(kexinit.client_parse( + self.common.config.as_ref(), + &self.common.cipher, + buf, + &mut self.common.write_buffer, + )?)); + } else if let Some(exchange) = std::mem::replace(&mut enc.exchange, None) { + let kexinit = KexInit::received_rekey( + exchange, + negotiation::Client::read_kex(buf, &self.common.config.as_ref().preferred)?, + &enc.session_id, + ); + enc.rekey = Some(Kex::KexDhDone(kexinit.client_parse( + self.common.config.as_ref(), + &mut self.common.cipher, + buf, + &mut self.common.write_buffer, + )?)); + } + } else { + unreachable!() + } + self.flush()?; + return Ok(self); + } + + if let Some(ref mut enc) = self.common.encrypted { + match enc.rekey.take() { + Some(Kex::KexDhDone(mut kexdhdone)) => { + if kexdhdone.names.ignore_guessed { + kexdhdone.names.ignore_guessed = false; + enc.rekey = Some(Kex::KexDhDone(kexdhdone)); + return Ok(self); + } else if buf[0] == msg::KEX_ECDH_REPLY { + // We've sent ECDH_INIT, waiting for ECDH_REPLY + enc.rekey = Some(kexdhdone.server_key_check(true, client, buf).await?); + self.common + .cipher + .write(&[msg::NEWKEYS], &mut self.common.write_buffer); + self.flush()?; + return Ok(self); + } else { + error!("Wrong packet received"); + return Err(Error::Inconsistent.into()); + } + } + Some(Kex::NewKeys(newkeys)) => { + if buf[0] != msg::NEWKEYS { + return Err(Error::Kex.into()); + } + self.common.write_buffer.bytes = 0; + enc.last_rekey = std::time::Instant::now(); + + // Ok, NEWKEYS received, now encrypted. + enc.flush_all_pending(); + let mut pending = std::mem::replace(&mut self.pending_reads, Vec::new()); + for p in pending.drain(..) { + self = self.process_packet(client, &p).await? + } + self.pending_reads = pending; + self.pending_len = 0; + self.common.newkeys(newkeys); + self.flush()?; + return Ok(self); + } + Some(Kex::KexInit(k)) => { + enc.rekey = Some(Kex::KexInit(k)); + self.pending_len += buf.len() as u32; + if self.pending_len > 2 * self.target_window_size { + return Err(Error::Pending.into()) + } + self.pending_reads.push(CryptoVec::from_slice(buf)); + return Ok(self); + } + rek => enc.rekey = rek, + } + } + self.process_packet(client, buf).await + } + + async fn process_packet( + mut self, + client: &mut Option, + buf: &[u8], + ) -> Result { + // If we've successfully read a packet. + debug!("buf = {:?} bytes", buf.len()); + trace!("buf = {:?}", buf); + let mut is_authenticated = false; + if let Some(ref mut enc) = self.common.encrypted { + match enc.state { + EncryptedState::WaitingServiceRequest { + ref mut accepted, .. + } => { + debug!( + "waiting service request, {:?} {:?}", + buf[0], + msg::SERVICE_ACCEPT + ); + if buf[0] == msg::SERVICE_ACCEPT { + let mut r = buf.reader(1); + if r.read_string().map_err(crate::Error::from)? == b"ssh-userauth" { + *accepted = true; + if let Some(ref meth) = self.common.auth_method { + let auth_request = auth::AuthRequest { + methods: auth::MethodSet::all(), + partial_success: false, + current: None, + rejection_count: 0, + }; + let len = enc.write.len(); + if enc.write_auth_request(&self.common.auth_user, meth) { + debug!("enc: {:?}", &enc.write[len..]); + enc.state = EncryptedState::WaitingAuthRequest(auth_request) + } + } else { + debug!("no auth method") + } + } + } else { + debug!("unknown message: {:?}", buf); + return Err(Error::Inconsistent.into()); + } + } + EncryptedState::WaitingAuthRequest(ref mut auth_request) => { + if buf[0] == msg::USERAUTH_SUCCESS { + debug!("userauth_success"); + self.sender + .send(Reply::AuthSuccess) + .map_err(|_| Error::SendError)?; + enc.state = EncryptedState::InitCompression; + enc.server_compression.init_decompress(&mut enc.decompress); + return Ok(self); + } else if buf[0] == msg::USERAUTH_BANNER { + let mut r = buf.reader(1); + let banner = r.read_string().map_err(crate::Error::from)?; + if let Ok(banner) = std::str::from_utf8(banner) { + let c = client.take().unwrap(); + let (c, s) = c.auth_banner(banner, self).await?; + *client = Some(c); + return Ok(s); + } else { + return Ok(self); + } + } else if buf[0] == msg::USERAUTH_FAILURE { + debug!("userauth_failure"); + + let mut r = buf.reader(1); + let remaining_methods = r.read_string().map_err(crate::Error::from)?; + debug!( + "remaining methods {:?}", + std::str::from_utf8(remaining_methods) + ); + auth_request.methods = auth::MethodSet::empty(); + for method in remaining_methods.split(|&c| c == b',') { + if let Some(m) = auth::MethodSet::from_bytes(method) { + auth_request.methods |= m + } + } + let no_more_methods = auth_request.methods.is_empty(); + self.common.auth_method = None; + self.sender + .send(Reply::AuthFailure) + .map_err(|_| Error::SendError)?; + + // If no other authentication method is allowed by the server, give up. + if no_more_methods { + return Err(Error::NoAuthMethod.into()); + } + } else if buf[0] == msg::USERAUTH_PK_OK { + debug!("userauth_pk_ok"); + if let Some(auth::CurrentRequest::PublicKey { + ref mut sent_pk_ok, .. + }) = auth_request.current + { + *sent_pk_ok = true; + } + + match self.common.auth_method.take() { + Some(auth_method @ auth::Method::PublicKey { .. }) => { + self.common.buffer.clear(); + enc.client_send_signature( + &self.common.auth_user, + &auth_method, + &mut self.common.buffer, + )? + } + Some(auth::Method::FuturePublicKey { key }) => { + debug!("public key"); + self.common.buffer.clear(); + let i = enc.client_make_to_sign( + &self.common.auth_user, + &key, + &mut self.common.buffer, + ); + let len = self.common.buffer.len(); + let buf = + std::mem::replace(&mut self.common.buffer, CryptoVec::new()); + + self.sender + .send(Reply::SignRequest { key, data: buf }) + .map_err(|_| Error::SendError)?; + self.common.buffer = loop { + match self.receiver.recv().await { + Some(Msg::Signed { data }) => break data, + _ => {} + } + }; + if self.common.buffer.len() != len { + // The buffer was modified. + push_packet!(enc.write, { + enc.write.extend(&self.common.buffer[i..]); + }) + } + } + _ => {} + } + } else { + debug!("unknown message: {:?}", buf); + return Err(Error::Inconsistent.into()); + } + } + EncryptedState::InitCompression => unreachable!(), + EncryptedState::Authenticated => is_authenticated = true, + } + } + if is_authenticated { + self.client_read_authenticated(client, buf).await + } else { + Ok(self) + } + } + + async fn client_read_authenticated( + mut self, + client: &mut Option, + buf: &[u8], + ) -> Result { + match buf[0] { + msg::CHANNEL_OPEN_CONFIRMATION => { + debug!("channel_open_confirmation"); + let mut reader = buf.reader(1); + let id_send = ChannelId(reader.read_u32().map_err(crate::Error::from)?); + let id_recv = reader.read_u32().map_err(crate::Error::from)?; + let window = reader.read_u32().map_err(crate::Error::from)?; + let max_packet = reader.read_u32().map_err(crate::Error::from)?; + + if let Some(ref mut enc) = self.common.encrypted { + if let Some(parameters) = enc.channels.get_mut(&id_send) { + parameters.recipient_channel = id_recv; + parameters.recipient_window_size = window; + parameters.recipient_maximum_packet_size = max_packet; + parameters.confirmed = true; + } else { + // We've not requested this channel, close connection. + return Err(Error::Inconsistent.into()); + } + } else { + return Err(Error::Inconsistent.into()); + }; + let c = client.take().unwrap(); + let (c, s) = c + .channel_open_confirmation(id_send, max_packet, window, self) + .await?; + *client = Some(c); + Ok(s) + } + msg::CHANNEL_CLOSE => { + debug!("channel_close"); + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + if let Some(ref mut enc) = self.common.encrypted { + enc.channels.remove(&channel_num); + } + let c = client.take().unwrap(); + let (c, s) = c.channel_close(channel_num, self).await?; + *client = Some(c); + Ok(s) + } + msg::CHANNEL_EOF => { + debug!("channel_eof"); + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + let c = client.take().unwrap(); + let (c, s) = c.channel_eof(channel_num, self).await?; + *client = Some(c); + Ok(s) + } + msg::CHANNEL_OPEN_FAILURE => { + debug!("channel_open_failure"); + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + let reason_code = + ChannelOpenFailure::from_u32(r.read_u32().map_err(crate::Error::from)?) + .unwrap(); + let descr = std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let language = std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + if let Some(ref mut enc) = self.common.encrypted { + enc.channels.remove(&channel_num); + } + let c = client.take().unwrap(); + let (c, s) = c + .channel_open_failure(channel_num, reason_code, descr, language, self) + .await?; + *client = Some(c); + Ok(s) + } + msg::CHANNEL_DATA => { + debug!("channel_data"); + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + let data = r.read_string().map_err(crate::Error::from)?; + let target = self.common.config.window_size; + let mut c = client.take().unwrap(); + if let Some(ref mut enc) = self.common.encrypted { + if enc.adjust_window_size(channel_num, data, target) { + let next_window = c.adjust_window(channel_num, self.target_window_size); + if next_window > 0 { + self.target_window_size = next_window + } + } + } + let (c, s) = c.data(channel_num, &data, self).await?; + *client = Some(c); + Ok(s) + } + msg::CHANNEL_EXTENDED_DATA => { + debug!("channel_extended_data"); + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + let extended_code = r.read_u32().map_err(crate::Error::from)?; + let data = r.read_string().map_err(crate::Error::from)?; + let target = self.common.config.window_size; + let mut c = client.take().unwrap(); + if let Some(ref mut enc) = self.common.encrypted { + if enc.adjust_window_size(channel_num, data, target) { + let next_window = c.adjust_window(channel_num, self.target_window_size); + if next_window > 0 { + self.target_window_size = next_window + } + } + } + let (c, s) = c + .extended_data(channel_num, extended_code, &data, self) + .await?; + *client = Some(c); + Ok(s) + } + msg::CHANNEL_REQUEST => { + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + let req = r.read_string().map_err(crate::Error::from)?; + debug!( + "channel_request: {:?} {:?}", + channel_num, + std::str::from_utf8(req) + ); + let cl = client.take().unwrap(); + let (c, s) = match req { + b"forwarded_tcpip" => { + let a = std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let b = r.read_u32().map_err(crate::Error::from)?; + let c = std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let d = r.read_u32().map_err(crate::Error::from)?; + cl.channel_open_forwarded_tcpip(channel_num, a, b, c, d, self) + .await? + } + b"xon-xoff" => { + r.read_byte().map_err(crate::Error::from)?; // should be 0. + let client_can_do = r.read_byte().map_err(crate::Error::from)?; + cl.xon_xoff(channel_num, client_can_do != 0, self).await? + } + b"exit-status" => { + r.read_byte().map_err(crate::Error::from)?; // should be 0. + let exit_status = r.read_u32().map_err(crate::Error::from)?; + cl.exit_status(channel_num, exit_status, self).await? + } + b"exit-signal" => { + r.read_byte().map_err(crate::Error::from)?; // should be 0. + let signal_name = + Sig::from_name(r.read_string().map_err(crate::Error::from)?)?; + let core_dumped = r.read_byte().map_err(crate::Error::from)?; + let error_message = + std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let lang_tag = + std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + cl.exit_signal( + channel_num, + signal_name, + core_dumped != 0, + error_message, + lang_tag, + self, + ) + .await? + } + _ => { + let wants_reply = r.read_byte().map_err(crate::Error::from)?; + if wants_reply == 1 { + if let Some(ref mut enc) = self.common.encrypted { + self.common.wants_reply = false; + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_FAILURE); + enc.write.push_u32_be(channel_num.0) + }) + } + } + info!( + "Unknown channel request {:?} {:?}", + std::str::from_utf8(req), + wants_reply + ); + (cl, self) + } + }; + *client = Some(c); + Ok(s) + } + msg::CHANNEL_WINDOW_ADJUST => { + debug!("channel_window_adjust"); + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + let amount = r.read_u32().map_err(crate::Error::from)?; + let mut new_value = 0; + debug!("amount: {:?}", amount); + if let Some(ref mut enc) = self.common.encrypted { + if let Some(ref mut channel) = enc.channels.get_mut(&channel_num) { + channel.recipient_window_size += amount; + new_value = channel.recipient_window_size; + } else { + return Err(Error::WrongChannel.into()); + } + } + let c = client.take().unwrap(); + let (c, s) = c.window_adjusted(channel_num, new_value, self).await?; + *client = Some(c); + Ok(s) + } + msg::GLOBAL_REQUEST => { + let mut r = buf.reader(1); + let req = r.read_string().map_err(crate::Error::from)?; + let wants_reply = r.read_byte().map_err(crate::Error::from)?; + if let Some(ref mut enc) = self.common.encrypted { + self.common.wants_reply = false; + push_packet!(enc.write, enc.write.push(msg::REQUEST_FAILURE)) + } + info!( + "Unhandled global request: {:?} {:?}", + std::str::from_utf8(req), + wants_reply + ); + Ok(self) + } + msg::CHANNEL_SUCCESS => { + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + let c = client.take().unwrap(); + let (c, s) = c.channel_success(channel_num, self).await?; + *client = Some(c); + Ok(s) + } + _ => { + info!("Unhandled packet: {:?}", buf); + Ok(self) + } + } + } + + pub(crate) fn write_auth_request_if_needed(&mut self, user: &str, meth: auth::Method) -> bool { + let mut is_waiting = false; + if let Some(ref mut enc) = self.common.encrypted { + is_waiting = match enc.state { + EncryptedState::WaitingAuthRequest(_) => true, + EncryptedState::WaitingServiceRequest { + accepted, + ref mut sent, + } => { + debug!("sending ssh-userauth service requset"); + if !*sent { + let p = b"\x05\0\0\0\x0Cssh-userauth"; + self.common.cipher.write(p, &mut self.common.write_buffer); + *sent = true + } + accepted + } + EncryptedState::InitCompression | EncryptedState::Authenticated => false, + }; + debug!( + "write_auth_request_if_needed: is_waiting = {:?}", + is_waiting + ); + if is_waiting { + enc.write_auth_request(user, &meth); + } + } + self.common.auth_user.clear(); + self.common.auth_user.push_str(user); + self.common.auth_method = Some(meth); + is_waiting + } +} + +impl Encrypted { + fn write_auth_request(&mut self, user: &str, auth_method: &auth::Method) -> bool { + // The server is waiting for our USERAUTH_REQUEST. + push_packet!(self.write, { + self.write.push(msg::USERAUTH_REQUEST); + + match *auth_method { + auth::Method::Password { ref password } => { + self.write.extend_ssh_string(user.as_bytes()); + self.write.extend_ssh_string(b"ssh-connection"); + self.write.extend_ssh_string(b"password"); + self.write.push(0); + self.write.extend_ssh_string(password.as_bytes()); + true + } + auth::Method::PublicKey { ref key } => { + self.write.extend_ssh_string(user.as_bytes()); + self.write.extend_ssh_string(b"ssh-connection"); + self.write.extend_ssh_string(b"publickey"); + self.write.push(0); // This is a probe + + debug!("write_auth_request: {:?}", key.name()); + self.write.extend_ssh_string(key.name().as_bytes()); + key.push_to(&mut self.write); + true + } + auth::Method::FuturePublicKey { ref key, .. } => { + self.write.extend_ssh_string(user.as_bytes()); + self.write.extend_ssh_string(b"ssh-connection"); + self.write.extend_ssh_string(b"publickey"); + self.write.push(0); // This is a probe + + self.write.extend_ssh_string(key.name().as_bytes()); + key.push_to(&mut self.write); + true + } + } + }) + } + + fn client_make_to_sign( + &mut self, + user: &str, + key: &Key, + buffer: &mut CryptoVec, + ) -> usize { + buffer.clear(); + buffer.extend_ssh_string(self.session_id.as_ref()); + + let i0 = buffer.len(); + buffer.push(msg::USERAUTH_REQUEST); + buffer.extend_ssh_string(user.as_bytes()); + buffer.extend_ssh_string(b"ssh-connection"); + buffer.extend_ssh_string(b"publickey"); + buffer.push(1); + buffer.extend_ssh_string(key.name().as_bytes()); + key.push_to(buffer); + i0 + } + + fn client_send_signature( + &mut self, + user: &str, + method: &auth::Method, + buffer: &mut CryptoVec, + ) -> Result<(), Error> { + match method { + &auth::Method::PublicKey { ref key } => { + let i0 = self.client_make_to_sign(user, key.as_ref(), buffer); + // Extend with self-signature. + key.add_self_signature(buffer)?; + push_packet!(self.write, { + self.write.extend(&buffer[i0..]); + }) + } + _ => {} + } + Ok(()) + } +} diff --git a/thrussh/src/client/kex.rs b/thrussh/src/client/kex.rs new file mode 100644 index 00000000..fc7de99e --- /dev/null +++ b/thrussh/src/client/kex.rs @@ -0,0 +1,68 @@ +use super::*; +use crate::cipher::CipherPair; +use crate::negotiation; +use crate::negotiation::Select; + +use crate::kex; + +impl KexInit { + pub fn client_parse( + mut self, + config: &Config, + cipher: &CipherPair, + buf: &[u8], + write_buffer: &mut SSHBuffer, + ) -> Result { + debug!("client parse {:?} {:?}", buf.len(), buf); + let algo = { + // read algorithms from packet. + debug!("extending {:?}", &self.exchange.server_kex_init[..]); + self.exchange.server_kex_init.extend(buf); + super::negotiation::Client::read_kex(buf, &config.preferred)? + }; + debug!("algo = {:?}", algo); + debug!("write = {:?}", &write_buffer.buffer[..]); + if !self.sent { + self.client_write(config, cipher, write_buffer)? + } + + // This function is called from the public API. + // + // In order to simplify the public API, we reuse the + // self.exchange.client_kex buffer to send an extra packet, + // then truncate that buffer. Without that, we would need an + // extra buffer. + let i0 = self.exchange.client_kex_init.len(); + debug!("i0 = {:?}", i0); + let kex = kex::Algorithm::client_dh( + algo.kex, + &mut self.exchange.client_ephemeral, + &mut self.exchange.client_kex_init, + )?; + + cipher.write(&self.exchange.client_kex_init[i0..], write_buffer); + self.exchange.client_kex_init.resize(i0); + + debug!("moving to kexdhdone, exchange = {:?}", self.exchange); + Ok(KexDhDone { + exchange: self.exchange, + names: algo, + kex: kex, + key: 0, + session_id: self.session_id, + }) + } + + pub fn client_write( + &mut self, + config: &Config, + cipher: &CipherPair, + write_buffer: &mut SSHBuffer, + ) -> Result<(), Error> { + self.exchange.client_kex_init.clear(); + negotiation::write_kex(&config.preferred, &mut self.exchange.client_kex_init)?; + self.sent = true; + cipher.write(&self.exchange.client_kex_init, write_buffer); + Ok(()) + } +} diff --git a/thrussh/src/client/mod.rs b/thrussh/src/client/mod.rs new file mode 100644 index 00000000..50c24abc --- /dev/null +++ b/thrussh/src/client/mod.rs @@ -0,0 +1,1448 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use crate::auth; +use crate::negotiation; +use crate::pty::Pty; +use crate::session::*; +use crate::ssh_read::SshRead; +use crate::sshbuffer::*; +use crate::{ChannelId, ChannelMsg, ChannelOpenFailure, Disconnect, Limits, Sig}; +use cryptovec::CryptoVec; +use futures::task::{Context, Poll}; +use futures::Future; +use std::cell::RefCell; +use std::collections::HashMap; +use std::pin::Pin; +use std::sync::Arc; +use thrussh_keys::encoding::{Encoding, Reader}; +use thrussh_keys::key; +use thrussh_keys::key::parse_public_key; +use tokio; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::pin; + +mod kex; +use crate::cipher; +use crate::{msg, Error}; +mod encrypted; +mod session; + +use tokio::sync::mpsc::*; +pub mod proxy; +pub struct Session { + common: CommonSession>, + receiver: Receiver, + sender: UnboundedSender, + channels: HashMap>, + target_window_size: u32, + pending_reads: Vec, + pending_len: u32, +} + +impl Drop for Session { + fn drop(&mut self) { + debug!("drop session") + } +} + +#[derive(Debug)] +enum Reply { + AuthSuccess, + AuthFailure, + ChannelOpenFailure, + SignRequest { + key: thrussh_keys::key::PublicKey, + data: CryptoVec, + }, +} + +#[derive(Debug)] +enum Msg { + Authenticate { + user: String, + method: auth::Method, + }, + Signed { + data: CryptoVec, + }, + ChannelOpenSession { + sender: UnboundedSender, + }, + ChannelOpenX11 { + originator_address: String, + originator_port: u32, + sender: UnboundedSender, + }, + ChannelOpenDirectTcpIp { + host_to_connect: String, + port_to_connect: u32, + originator_address: String, + originator_port: u32, + sender: UnboundedSender, + }, + TcpIpForward { + want_reply: bool, + address: String, + port: u32, + }, + CancelTcpIpForward { + want_reply: bool, + address: String, + port: u32, + }, + Disconnect { + reason: Disconnect, + description: String, + language_tag: String, + }, + Data { + id: ChannelId, + data: CryptoVec, + }, + ExtendedData { + id: ChannelId, + data: CryptoVec, + ext: u32, + }, + Eof { + id: ChannelId, + }, + RequestPty { + id: ChannelId, + want_reply: bool, + term: String, + col_width: u32, + row_height: u32, + pix_width: u32, + pix_height: u32, + terminal_modes: Vec<(Pty, u32)>, + }, + RequestShell { + id: ChannelId, + want_reply: bool, + }, + Exec { + id: ChannelId, + want_reply: bool, + command: String, + }, + Signal { + id: ChannelId, + signal: Sig, + }, + RequestSubsystem { + id: ChannelId, + want_reply: bool, + name: String, + }, + RequestX11 { + id: ChannelId, + want_reply: bool, + single_connection: bool, + x11_authentication_protocol: String, + x11_authentication_cookie: String, + x11_screen_number: u32, + }, + SetEnv { + id: ChannelId, + want_reply: bool, + variable_name: String, + variable_value: String, + }, + WindowChange { + id: ChannelId, + col_width: u32, + row_height: u32, + pix_width: u32, + pix_height: u32, + }, +} + +#[derive(Debug)] +enum OpenChannelMsg { + Open { + id: ChannelId, + max_packet_size: u32, + window_size: u32, + }, + Msg(ChannelMsg), +} + +/// Handle to a session, used to send messages to a client outside of +/// the request/response cycle. +pub struct Handle { + sender: Sender, + receiver: UnboundedReceiver, + join: tokio::task::JoinHandle>, +} + +impl Drop for Handle { + fn drop(&mut self) { + debug!("drop handle") + } +} + +#[derive(Clone)] +pub struct ChannelSender { + sender: Sender, + id: ChannelId, +} + +pub struct Channel { + sender: ChannelSender, + receiver: UnboundedReceiver, + max_packet_size: u32, + window_size: u32, +} + +impl Handle { + + pub fn is_closed(&self) -> bool { + self.sender.is_closed() + } + + pub async fn authenticate_password, P: Into>( + &mut self, + user: U, + password: P, + ) -> Result { + let user = user.into(); + self.sender + .send(Msg::Authenticate { + user, + method: auth::Method::Password { + password: password.into(), + }, + }) + .await + .map_err(|_| Error::SendError)?; + loop { + match self.receiver.recv().await { + Some(Reply::AuthSuccess) => return Ok(true), + Some(Reply::AuthFailure) => return Ok(false), + None => return Ok(false), + _ => {} + } + } + } + + pub async fn authenticate_publickey>( + &mut self, + user: U, + key: Arc, + ) -> Result { + let user = user.into(); + self.sender + .send(Msg::Authenticate { + user, + method: auth::Method::PublicKey { key }, + }) + .await + .map_err(|_| Error::SendError)?; + loop { + match self.receiver.recv().await { + Some(Reply::AuthSuccess) => return Ok(true), + Some(Reply::AuthFailure) => return Ok(false), + None => return Ok(false), + _ => {} + } + } + } + + pub async fn authenticate_future, S: auth::Signer>( + &mut self, + user: U, + key: key::PublicKey, + mut future: S, + ) -> (S, Result) { + let user = user.into(); + if let Err(_) = self + .sender + .send(Msg::Authenticate { + user, + method: auth::Method::FuturePublicKey { key }, + }) + .await + { + return (future, Err((crate::SendError {}).into())); + } + loop { + let reply = self.receiver.recv().await; + match reply { + Some(Reply::AuthSuccess) => return (future, Ok(true)), + Some(Reply::AuthFailure) => return (future, Ok(false)), + Some(Reply::SignRequest { key, data }) => { + let (f, data) = future.auth_publickey_sign(&key, data).await; + future = f; + let data = match data { + Ok(data) => data, + Err(e) => return (future, Err(e.into())), + }; + if let Err(_) = self.sender.send(Msg::Signed { data }).await { + return (future, Err((crate::SendError {}).into())); + } + } + None => return (future, Ok(false)), + _ => {} + } + } + } + + async fn wait_channel_confirmation( + &self, + mut receiver: UnboundedReceiver, + ) -> Result { + loop { + match receiver.recv().await { + Some(OpenChannelMsg::Open { + id, + max_packet_size, + window_size, + }) => { + return Ok(Channel { + sender: ChannelSender { + sender: self.sender.clone(), + id, + }, + receiver, + max_packet_size, + window_size, + }); + } + None => { + return Err(Error::Disconnect.into()); + } + msg => { + debug!("msg = {:?}", msg); + } + } + } + } + + /// Request a session channel (the most basic type of + /// channel). This function returns `Some(..)` immediately if the + /// connection is authenticated, but the channel only becomes + /// usable when it's confirmed by the server, as indicated by the + /// `confirmed` field of the corresponding `Channel`. + pub async fn channel_open_session(&mut self) -> Result { + let (sender, receiver) = unbounded_channel(); + self.sender + .send(Msg::ChannelOpenSession { sender }) + .await + .map_err(|_| Error::SendError)?; + self.wait_channel_confirmation(receiver).await + } + + /// Request an X11 channel, on which the X11 protocol may be tunneled. + pub async fn channel_open_x11>( + &mut self, + originator_address: A, + originator_port: u32, + ) -> Result { + let (sender, receiver) = unbounded_channel(); + self.sender + .send(Msg::ChannelOpenX11 { + originator_address: originator_address.into(), + originator_port, + sender, + }) + .await + .map_err(|_| Error::SendError)?; + self.wait_channel_confirmation(receiver).await + } + + /// Open a TCP/IP forwarding channel. This is usually done when a + /// connection comes to a locally forwarded TCP/IP port. See + /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-7). The + /// TCP/IP packets can then be tunneled through the channel using + /// `.data()`. + pub async fn channel_open_direct_tcpip, B: Into>( + &mut self, + host_to_connect: A, + port_to_connect: u32, + originator_address: B, + originator_port: u32, + ) -> Result { + let (sender, receiver) = unbounded_channel(); + self.sender + .send(Msg::ChannelOpenDirectTcpIp { + host_to_connect: host_to_connect.into(), + port_to_connect, + originator_address: originator_address.into(), + originator_port, + sender, + }) + .await + .map_err(|_| Error::SendError)?; + self.wait_channel_confirmation(receiver).await + } + + /// Sends a disconnect message. + pub async fn disconnect( + &mut self, + reason: Disconnect, + description: &str, + language_tag: &str, + ) -> Result<(), Error> { + self.sender + .send(Msg::Disconnect { + reason, + description: description.into(), + language_tag: language_tag.into(), + }) + .await + .map_err(|_| Error::SendError)?; + Ok(()) + } +} + +impl Channel { + pub fn id(&self) -> ChannelId { + self.sender.id + } + + /// Returns the min between the maximum packet size and the + /// remaining window size in the channel. + pub fn writable_packet_size(&self) -> usize { + self.max_packet_size.min(self.window_size) as usize + } + + /// Request a pseudo-terminal with the given characteristics. + pub async fn request_pty( + &mut self, + want_reply: bool, + term: &str, + col_width: u32, + row_height: u32, + pix_width: u32, + pix_height: u32, + terminal_modes: &[(Pty, u32)], + ) -> Result<(), Error> { + self.sender + .sender + .send(Msg::RequestPty { + id: self.sender.id, + want_reply, + term: term.to_string(), + col_width, + row_height, + pix_width, + pix_height, + terminal_modes: terminal_modes.to_vec(), + }) + .await + .map_err(|_| Error::SendError)?; + Ok(()) + } + + /// Request a remote shell. + pub async fn request_shell(&mut self, want_reply: bool) -> Result<(), Error> { + self.sender + .sender + .send(Msg::RequestShell { + id: self.sender.id, + want_reply, + }) + .await + .map_err(|_| Error::SendError)?; + Ok(()) + } + + /// Execute a remote program (will be passed to a shell). This can + /// be used to implement scp (by calling a remote scp and + /// tunneling to its standard input). + pub async fn exec>( + &mut self, + want_reply: bool, + command: A, + ) -> Result<(), Error> { + self.sender + .sender + .send(Msg::Exec { + id: self.sender.id, + want_reply, + command: command.into(), + }) + .await + .map_err(|e| { + debug!("e = {:?}", e); + Error::SendError + })?; + Ok(()) + } + + /// Signal a remote process. + pub async fn signal(&mut self, signal: Sig) -> Result<(), Error> { + self.sender + .sender + .send(Msg::Signal { + id: self.sender.id, + signal, + }) + .await + .map_err(|_| Error::SendError)?; + Ok(()) + } + + /// Request the start of a subsystem with the given name. + pub async fn request_subsystem>( + &mut self, + want_reply: bool, + name: A, + ) -> Result<(), Error> { + self.sender + .sender + .send(Msg::RequestSubsystem { + id: self.sender.id, + want_reply, + name: name.into(), + }) + .await + .map_err(|_| Error::SendError)?; + Ok(()) + } + + /// Request the forwarding of a remote port to the client. The + /// server will then open forwarding channels (which cause the + /// client to call `.channel_open_forwarded_tcpip()`). + pub async fn tcpip_forward>( + &mut self, + want_reply: bool, + address: A, + port: u32, + ) -> Result<(), Error> { + self.sender + .sender + .send(Msg::TcpIpForward { + want_reply, + address: address.into(), + port, + }) + .await + .map_err(|_| Error::SendError)?; + Ok(()) + } + + /// Cancel a previous forwarding request. + pub async fn cancel_tcpip_forward>( + &mut self, + want_reply: bool, + address: A, + port: u32, + ) -> Result<(), Error> { + self.sender + .sender + .send(Msg::CancelTcpIpForward { + want_reply, + address: address.into(), + port, + }) + .await + .map_err(|_| Error::SendError)?; + Ok(()) + } + + /// Request X11 forwarding through an already opened X11 + /// channel. See + /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-6.3.1) + /// for security issues related to cookies. + pub async fn request_x11, B: Into>( + &mut self, + want_reply: bool, + single_connection: bool, + x11_authentication_protocol: A, + x11_authentication_cookie: B, + x11_screen_number: u32, + ) -> Result<(), Error> { + self.sender + .sender + .send(Msg::RequestX11 { + id: self.sender.id, + want_reply, + single_connection, + x11_authentication_protocol: x11_authentication_protocol.into(), + x11_authentication_cookie: x11_authentication_cookie.into(), + x11_screen_number, + }) + .await + .map_err(|_| Error::SendError)?; + Ok(()) + } + + /// Set a remote environment variable. + pub async fn set_env, B: Into>( + &mut self, + want_reply: bool, + variable_name: A, + variable_value: B, + ) -> Result<(), Error> { + self.sender + .sender + .send(Msg::SetEnv { + id: self.sender.id, + want_reply, + variable_name: variable_name.into(), + variable_value: variable_value.into(), + }) + .await + .map_err(|_| Error::SendError)?; + Ok(()) + } + + /// Inform the server that our window size has changed. + pub async fn window_change( + &mut self, + col_width: u32, + row_height: u32, + pix_width: u32, + pix_height: u32, + ) -> Result<(), Error> { + self.sender + .sender + .send(Msg::WindowChange { + id: self.sender.id, + col_width, + row_height, + pix_width, + pix_height, + }) + .await + .map_err(|_| Error::SendError)?; + Ok(()) + } + + /// Send data to a channel. + pub async fn data( + &mut self, + data: R, + ) -> Result<(), Error> { + self.send_data(None, data).await + } + + /// Send data to a channel. The number of bytes added to the + /// "sending pipeline" (to be processed by the event loop) is + /// returned. + pub async fn extended_data( + &mut self, + ext: u32, + data: R, + ) -> Result<(), Error> { + self.send_data(Some(ext), data).await + } + + async fn send_data( + &mut self, + ext: Option, + mut data: R, + ) -> Result<(), Error> { + let mut total = 0; + loop { + // wait for the window to be restored. + while self.window_size == 0 { + match self.receiver.recv().await { + Some(OpenChannelMsg::Msg(ChannelMsg::WindowAdjusted { new_size })) => { + debug!("window adjusted: {:?}", new_size); + self.window_size = new_size; + break; + } + Some(OpenChannelMsg::Msg(msg)) => { + debug!("unexpected channel msg: {:?}", msg); + } + Some(_) => debug!("unexpected channel msg"), + None => break, + } + } + debug!( + "sending data, self.window_size = {:?}, self.max_packet_size = {:?}, total = {:?}", + self.window_size, self.max_packet_size, total + ); + let sendable = self.window_size.min(self.max_packet_size) as usize; + debug!("sendable {:?}", sendable); + let mut c = CryptoVec::new_zeroed(sendable); + let n = data.read(&mut c[..]).await?; + total += n; + c.resize(n); + self.window_size -= n as u32; + self.send_data_packet(ext, c).await?; + if n == 0 { + break; + } else if self.window_size > 0 { + continue; + } + } + Ok(()) + } + + async fn send_data_packet(&mut self, ext: Option, data: CryptoVec) -> Result<(), Error> { + self.sender + .sender + .send(if let Some(ext) = ext { + Msg::ExtendedData { + id: self.sender.id, + ext, + data, + } + } else { + Msg::Data { + id: self.sender.id, + data, + } + }) + .await + .map_err(|e| { + error!("{:?}", e); + Error::SendError + })?; + Ok(()) + } + + pub async fn eof(&mut self) -> Result<(), Error> { + self.sender + .sender + .send(Msg::Eof { id: self.sender.id }) + .await + .map_err(|_| Error::SendError)?; + Ok(()) + } + + /// Wait for data to come. + pub async fn wait(&mut self) -> Option { + loop { + match self.receiver.recv().await { + Some(OpenChannelMsg::Msg(ChannelMsg::WindowAdjusted { new_size })) => { + self.window_size += new_size; + return Some(ChannelMsg::WindowAdjusted { new_size }); + } + Some(OpenChannelMsg::Msg(msg)) => return Some(msg), + None => return None, + _ => {} + } + } + } +} + +impl Future for Handle { + type Output = Result<(), H::Error>; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + match Future::poll(Pin::new(&mut self.join), cx) { + Poll::Ready(r) => Poll::Ready(match r { + Ok(Ok(x)) => Ok(x), + Err(e) => Err(crate::Error::from(e).into()), + Ok(Err(e)) => Err(e), + }), + Poll::Pending => Poll::Pending, + } + } +} + +use std::net::ToSocketAddrs; +pub async fn connect( + config: Arc, + addr: T, + handler: H, +) -> Result, H::Error> { + let addr = addr + .to_socket_addrs() + .map_err(crate::Error::from)? + .next() + .unwrap(); + let socket = TcpStream::connect(addr).await.map_err(crate::Error::from)?; + connect_stream(config, socket, handler).await +} + +pub async fn connect_stream( + config: Arc, + mut stream: R, + handler: H, +) -> Result, H::Error> +where + H: Handler + Send + 'static, + R: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + // Writing SSH id. + let mut write_buffer = SSHBuffer::new(); + write_buffer.send_ssh_id(config.as_ref().client_id.as_bytes()); + stream + .write_all(&write_buffer.buffer) + .await + .map_err(crate::Error::from)?; + + // Reading SSH id and allocating a session if correct. + let mut stream = SshRead::new(stream); + let sshid = stream.read_ssh_id().await?; + let (sender, receiver) = channel(10); + let (sender2, receiver2) = unbounded_channel(); + if config.maximum_packet_size > 65535 { + error!( + "Maximum packet size ({:?}) should not larger than a TCP packet (65535)", + config.maximum_packet_size + ); + } + let mut session = Session { + target_window_size: config.window_size, + common: CommonSession { + write_buffer, + kex: None, + auth_user: String::new(), + auth_method: None, // Client only. + cipher: Arc::new(cipher::CLEAR_PAIR), + encrypted: None, + config, + wants_reply: false, + disconnected: false, + buffer: CryptoVec::new(), + }, + receiver, + sender: sender2, + channels: HashMap::new(), + pending_reads: Vec::new(), + pending_len: 0, + }; + session.read_ssh_id(sshid)?; + let (encrypted_signal, encrypted_recv) = tokio::sync::oneshot::channel(); + let join = tokio::spawn(session.run(stream, handler, Some(encrypted_signal))); + encrypted_recv.await.unwrap_or(()); + Ok(Handle { + sender, + receiver: receiver2, + join, + }) +} + +async fn start_reading( + mut stream_read: R, + mut buffer: SSHBuffer, + cipher: Arc, +) -> Result<(usize, R, SSHBuffer), Error> { + buffer.buffer.clear(); + let n = cipher::read(&mut stream_read, &mut buffer, &cipher).await?; + Ok((n, stream_read, buffer)) +} + +impl Session { + async fn run( + mut self, + mut stream: SshRead, + handler: H, + mut encrypted_signal: Option>, + ) -> Result<(), H::Error> { + self.flush()?; + if !self.common.write_buffer.buffer.is_empty() { + debug!("writing {:?} bytes", self.common.write_buffer.buffer.len()); + stream + .write_all(&self.common.write_buffer.buffer) + .await + .map_err(crate::Error::from)?; + stream.flush().await.map_err(crate::Error::from)?; + } + self.common.write_buffer.buffer.clear(); + let mut decomp = CryptoVec::new(); + let mut handler = Some(handler); + + let (stream_read, mut stream_write) = stream.split(); + let buffer = SSHBuffer::new(); + let reading = start_reading(stream_read, buffer, self.common.cipher.clone()); + pin!(reading); + + while !self.common.disconnected { + tokio::select! { + r = &mut reading => { + let (stream_read, buffer) = match r { + Ok((_, stream_read, buffer)) => (stream_read, buffer), + Err(e) => return Err(e.into()) + }; + if buffer.buffer.len() < 5 { + break + } + let buf = if let Some(ref mut enc) = self.common.encrypted { + if let Ok(buf) = enc.decompress.decompress( + &buffer.buffer[5..], + &mut decomp, + ) { + buf + } else { + break + } + } else { + &buffer.buffer[5..] + }; + if !buf.is_empty() { + if buf[0] == crate::msg::DISCONNECT { + break; + } else if buf[0] > 4 { + self = reply(self, &mut handler, &mut encrypted_signal, &buf[..]).await?; + } + } + reading.set(start_reading(stream_read, buffer, self.common.cipher.clone())); + } + msg = self.receiver.recv(), if !self.is_rekeying() => { + match msg { + Some(Msg::Authenticate { user, method }) => { + self.write_auth_request_if_needed(&user, method); + } + Some(Msg::Signed { .. }) => {}, + Some(Msg::ChannelOpenSession { sender }) => { + let id = self.channel_open_session()?; + self.channels.insert(id, sender); + } + Some(Msg::ChannelOpenX11 { originator_address, originator_port, sender }) => { + let id = self.channel_open_x11(&originator_address, originator_port)?; + self.channels.insert(id, sender); + } + Some(Msg::ChannelOpenDirectTcpIp { host_to_connect, port_to_connect, originator_address, originator_port, sender }) => { + let id = self.channel_open_direct_tcpip(&host_to_connect, port_to_connect, &originator_address, originator_port)?; + self.channels.insert(id, sender); + } + Some(Msg::TcpIpForward { want_reply, address, port }) => { + self.tcpip_forward(want_reply, &address, port) + }, + Some(Msg::CancelTcpIpForward { want_reply, address, port }) => { + self.cancel_tcpip_forward(want_reply, &address, port) + }, + Some(Msg::Disconnect { reason, description, language_tag }) => { + self.disconnect(reason, &description, &language_tag) + }, + Some(Msg::Data { data, id }) => { self.data(id, data) }, + Some(Msg::Eof { id }) => { self.eof(id); }, + Some(Msg::ExtendedData { data, ext, id }) => { self.extended_data(id, ext, data); }, + Some(Msg::RequestPty { id, want_reply, term, col_width, row_height, pix_width, pix_height, terminal_modes }) => { + self.request_pty(id, want_reply, &term, col_width, row_height, pix_width, pix_height, &terminal_modes) + }, + Some(Msg::WindowChange { id, col_width, row_height, pix_width, pix_height }) => { + self.window_change(id, col_width, row_height, pix_width, pix_height) + }, + Some(Msg::RequestX11 { id, want_reply, single_connection, x11_authentication_protocol, x11_authentication_cookie, x11_screen_number }) => { + self.request_x11(id, want_reply, single_connection, &x11_authentication_protocol, &x11_authentication_cookie, x11_screen_number) + }, + Some(Msg::SetEnv { id, want_reply, variable_name, variable_value }) => { + self.set_env(id, want_reply, &variable_name, &variable_value) + }, + Some(Msg::RequestShell { id, want_reply }) => { + self.request_shell(want_reply, id) + }, + Some(Msg::Exec { id, want_reply, command }) => { + self.exec(id, want_reply, &command) + }, + Some(Msg::Signal { id, signal }) => { + self.signal(id, signal) + }, + Some(Msg::RequestSubsystem { id, want_reply, name }) => { + self.request_subsystem(want_reply, id, &name) + }, + None => { + self.common.disconnected = true; + break + } + } + } + } + self.flush()?; + if !self.common.write_buffer.buffer.is_empty() { + debug!( + "writing to stream: {:?} bytes", + self.common.write_buffer.buffer.len() + ); + stream_write + .write_all(&self.common.write_buffer.buffer) + .await + .map_err(crate::Error::from)?; + stream_write.flush().await.map_err(crate::Error::from)?; + } + self.common.write_buffer.buffer.clear(); + if let Some(ref mut enc) = self.common.encrypted { + if let EncryptedState::InitCompression = enc.state { + enc.client_compression.init_compress(&mut enc.compress); + enc.state = EncryptedState::Authenticated; + } + } + } + debug!("disconnected"); + if self.common.disconnected { + stream_write.shutdown().await.map_err(crate::Error::from)?; + } + Ok(()) + } + + fn is_rekeying(&self) -> bool { + if let Some(ref enc) = self.common.encrypted { + enc.rekey.is_some() + } else { + true + } + } + + fn read_ssh_id(&mut self, sshid: &[u8]) -> Result<(), Error> { + // self.read_buffer.bytes += sshid.bytes_read + 2; + let mut exchange = Exchange::new(); + exchange.server_id.extend(sshid); + // Preparing the response + exchange + .client_id + .extend(self.common.config.as_ref().client_id.as_bytes()); + let mut kexinit = KexInit { + exchange: exchange, + algo: None, + sent: false, + session_id: None, + }; + self.common.write_buffer.buffer.clear(); + kexinit.client_write( + self.common.config.as_ref(), + &mut self.common.cipher, + &mut self.common.write_buffer, + )?; + self.common.kex = Some(Kex::KexInit(kexinit)); + Ok(()) + } + + /// Flush the temporary cleartext buffer into the encryption + /// buffer. This does *not* flush to the socket. + fn flush(&mut self) -> Result<(), Error> { + if let Some(ref mut enc) = self.common.encrypted { + if enc.flush( + &self.common.config.as_ref().limits, + &mut self.common.cipher, + &mut self.common.write_buffer, + ) { + info!("Re-exchanging keys"); + if enc.rekey.is_none() { + if let Some(exchange) = std::mem::replace(&mut enc.exchange, None) { + let mut kexinit = KexInit::initiate_rekey(exchange, &enc.session_id); + kexinit.client_write( + &self.common.config.as_ref(), + &mut self.common.cipher, + &mut self.common.write_buffer, + )?; + enc.rekey = Some(Kex::KexInit(kexinit)) + } + } + } + } + Ok(()) + } + + /// Send a `ChannelMsg` from the background handler to the client. + pub fn send_channel_msg(&self, channel: ChannelId, msg: ChannelMsg) -> bool { + if let Some(chan) = self.channels.get(&channel) { + chan.send(OpenChannelMsg::Msg(msg)).unwrap_or(()); + true + } else { + false + } + } +} +thread_local! { + static HASH_BUFFER: RefCell = RefCell::new(CryptoVec::new()); +} + +impl KexDhDone { + async fn server_key_check( + mut self, + rekey: bool, + handler: &mut Option, + buf: &[u8], + ) -> Result { + let mut reader = buf.reader(1); + let pubkey = reader.read_string().map_err(crate::Error::from)?; // server public key. + let pubkey = parse_public_key(pubkey).map_err(crate::Error::from)?; + debug!("server_public_Key: {:?}", pubkey); + if !rekey { + let h = handler.take().unwrap(); + let (h, check) = h.check_server_key(&pubkey).await?; + *handler = Some(h); + if !check { + return Err(Error::UnknownKey.into()); + } + } + HASH_BUFFER.with(|buffer| { + let mut buffer = buffer.borrow_mut(); + buffer.clear(); + let hash = { + let server_ephemeral = reader.read_string().map_err(crate::Error::from)?; + self.exchange.server_ephemeral.extend(server_ephemeral); + let signature = reader.read_string().map_err(crate::Error::from)?; + + self.kex + .compute_shared_secret(&self.exchange.server_ephemeral)?; + debug!("kexdhdone.exchange = {:?}", self.exchange); + let hash = self + .kex + .compute_exchange_hash(&pubkey, &self.exchange, &mut buffer)?; + debug!("exchange hash: {:?}", hash); + let signature = { + let mut sig_reader = signature.reader(0); + let sig_type = sig_reader.read_string().map_err(crate::Error::from)?; + debug!("sig_type: {:?}", sig_type); + sig_reader.read_string().map_err(crate::Error::from)? + }; + use thrussh_keys::key::Verify; + debug!("signature: {:?}", signature); + if !pubkey.verify_server_auth(hash.as_ref(), signature) { + debug!("wrong server sig"); + return Err(Error::WrongServerSig.into()); + } + hash + }; + let mut newkeys = self.compute_keys(hash, false)?; + newkeys.sent = true; + Ok(Kex::NewKeys(newkeys)) + }) + } +} + +async fn reply( + mut session: Session, + handler: &mut Option, + sender: &mut Option>, + buf: &[u8], +) -> Result { + match session.common.kex.take() { + Some(Kex::KexInit(kexinit)) => { + if kexinit.algo.is_some() + || buf[0] == msg::KEXINIT + || session.common.encrypted.is_none() + { + session.common.kex = Some(Kex::KexDhDone(kexinit.client_parse( + session.common.config.as_ref(), + &session.common.cipher, + buf, + &mut session.common.write_buffer, + )?)); + session.flush()?; + } + Ok(session) + } + Some(Kex::KexDhDone(mut kexdhdone)) => { + if kexdhdone.names.ignore_guessed { + kexdhdone.names.ignore_guessed = false; + session.common.kex = Some(Kex::KexDhDone(kexdhdone)); + Ok(session) + } else if buf[0] == msg::KEX_ECDH_REPLY { + // We've sent ECDH_INIT, waiting for ECDH_REPLY + session.common.kex = Some(kexdhdone.server_key_check(false, handler, buf).await?); + session + .common + .cipher + .write(&[msg::NEWKEYS], &mut session.common.write_buffer); + session.flush()?; + Ok(session) + } else { + error!("Wrong packet received"); + Err(Error::Inconsistent.into()) + } + } + Some(Kex::NewKeys(newkeys)) => { + debug!("newkeys received"); + if buf[0] != msg::NEWKEYS { + return Err(Error::Kex.into()); + } + if let Some(sender) = sender.take() { + sender.send(()).unwrap_or(()); + } + session.common.encrypted( + EncryptedState::WaitingServiceRequest { + accepted: false, + sent: false, + }, + newkeys, + ); + // Ok, NEWKEYS received, now encrypted. + Ok(session) + } + Some(kex) => { + session.common.kex = Some(kex); + Ok(session) + } + None => session.client_read_encrypted(handler, buf).await, + } +} + +/// The configuration of clients. +#[derive(Debug)] +pub struct Config { + /// The client ID string sent at the beginning of the protocol. + pub client_id: String, + /// The bytes and time limits before key re-exchange. + pub limits: Limits, + /// The initial size of a channel (used for flow control). + pub window_size: u32, + /// The maximal size of a single packet. + pub maximum_packet_size: u32, + /// Lists of preferred algorithms. + pub preferred: negotiation::Preferred, + /// Time after which the connection is garbage-collected. + pub connection_timeout: Option, +} + +impl Default for Config { + fn default() -> Config { + Config { + client_id: format!( + "SSH-2.0-{}_{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + ), + limits: Limits::default(), + window_size: 2097152, + maximum_packet_size: 32768, + preferred: Default::default(), + connection_timeout: None, + } + } +} + +/// A client handler. Note that messages can be received from the +/// server at any time during a session. +pub trait Handler: Sized { + type Error: From + Send; + /// A future ultimately resolving into a boolean, which can be + /// returned by some parts of this handler. + type FutureBool: Future> + Send; + + /// A future ultimately resolving into unit, which can be + /// returned by some parts of this handler. + type FutureUnit: Future> + Send; + + /// Convert a `bool` to `Self::FutureBool`. This is used to + /// produce the default handlers. + fn finished_bool(self, b: bool) -> Self::FutureBool; + + /// Produce a `Self::FutureUnit`. This is used to produce the + /// default handlers. + fn finished(self, session: Session) -> Self::FutureUnit; + + /// Called when the server sends us an authentication banner. This + /// is usually meant to be shown to the user, see + /// [RFC4252](https://tools.ietf.org/html/rfc4252#section-5.4) for + /// more details. + /// + /// The returned Boolean is ignored. + #[allow(unused_variables)] + fn auth_banner(self, banner: &str, session: Session) -> Self::FutureUnit { + self.finished(session) + } + + /// Called to check the server's public key. This is a very important + /// step to help prevent man-in-the-middle attacks. The default + /// implementation rejects all keys. + #[allow(unused_variables)] + fn check_server_key(self, server_public_key: &key::PublicKey) -> Self::FutureBool { + self.finished_bool(false) + } + + /// Called when the server confirmed our request to open a + /// channel. A channel can only be written to after receiving this + /// message (this library panics otherwise). + #[allow(unused_variables)] + fn channel_open_confirmation( + self, + id: ChannelId, + max_packet_size: u32, + window_size: u32, + session: Session, + ) -> Self::FutureUnit { + if let Some(channel) = session.channels.get(&id) { + channel + .send(OpenChannelMsg::Open { + id, + max_packet_size, + window_size, + }) + .unwrap_or(()); + } else { + error!("no channel for id {:?}", id); + } + self.finished(session) + } + + /// Called when the server signals success. + #[allow(unused_variables)] + fn channel_success(self, channel: ChannelId, session: Session) -> Self::FutureUnit { + if let Some(chan) = session.channels.get(&channel) { + chan.send(OpenChannelMsg::Msg(ChannelMsg::Success)) + .unwrap_or(()) + } + self.finished(session) + } + + /// Called when the server closes a channel. + #[allow(unused_variables)] + fn channel_close(self, channel: ChannelId, mut session: Session) -> Self::FutureUnit { + session.channels.remove(&channel); + self.finished(session) + } + + /// Called when the server sends EOF to a channel. + #[allow(unused_variables)] + fn channel_eof(self, channel: ChannelId, session: Session) -> Self::FutureUnit { + if let Some(chan) = session.channels.get(&channel) { + chan.send(OpenChannelMsg::Msg(ChannelMsg::Eof)) + .unwrap_or(()) + } + self.finished(session) + } + + /// Called when the server rejected our request to open a channel. + #[allow(unused_variables)] + fn channel_open_failure( + self, + channel: ChannelId, + reason: ChannelOpenFailure, + description: &str, + language: &str, + mut session: Session, + ) -> Self::FutureUnit { + session.channels.remove(&channel); + session.sender.send(Reply::ChannelOpenFailure).unwrap_or(()); + self.finished(session) + } + + /// Called when a new channel is created. + #[allow(unused_variables)] + fn channel_open_forwarded_tcpip( + self, + channel: ChannelId, + connected_address: &str, + connected_port: u32, + originator_address: &str, + originator_port: u32, + session: Session, + ) -> Self::FutureUnit { + self.finished(session) + } + + /// Called when the server sends us data. The `extended_code` + /// parameter is a stream identifier, `None` is usually the + /// standard output, and `Some(1)` is the standard error. See + /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-5.2). + #[allow(unused_variables)] + fn data(self, channel: ChannelId, data: &[u8], session: Session) -> Self::FutureUnit { + if let Some(chan) = session.channels.get(&channel) { + chan.send(OpenChannelMsg::Msg(ChannelMsg::Data { + data: CryptoVec::from_slice(data), + })) + .unwrap_or(()) + } + self.finished(session) + } + + /// Called when the server sends us data. The `extended_code` + /// parameter is a stream identifier, `None` is usually the + /// standard output, and `Some(1)` is the standard error. See + /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-5.2). + #[allow(unused_variables)] + fn extended_data( + self, + channel: ChannelId, + ext: u32, + data: &[u8], + session: Session, + ) -> Self::FutureUnit { + if let Some(chan) = session.channels.get(&channel) { + chan.send(OpenChannelMsg::Msg(ChannelMsg::ExtendedData { + ext, + data: CryptoVec::from_slice(data), + })) + .unwrap_or(()) + } + self.finished(session) + } + + /// The server informs this client of whether the client may + /// perform control-S/control-Q flow control. See + /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-6.8). + #[allow(unused_variables)] + fn xon_xoff( + self, + channel: ChannelId, + client_can_do: bool, + session: Session, + ) -> Self::FutureUnit { + if let Some(chan) = session.channels.get(&channel) { + chan.send(OpenChannelMsg::Msg(ChannelMsg::XonXoff { client_can_do })) + .unwrap_or(()) + } + self.finished(session) + } + + /// The remote process has exited, with the given exit status. + #[allow(unused_variables)] + fn exit_status( + self, + channel: ChannelId, + exit_status: u32, + session: Session, + ) -> Self::FutureUnit { + if let Some(chan) = session.channels.get(&channel) { + chan.send(OpenChannelMsg::Msg(ChannelMsg::ExitStatus { exit_status })) + .unwrap_or(()) + } + self.finished(session) + } + + /// The remote process exited upon receiving a signal. + #[allow(unused_variables)] + fn exit_signal( + self, + channel: ChannelId, + signal_name: Sig, + core_dumped: bool, + error_message: &str, + lang_tag: &str, + session: Session, + ) -> Self::FutureUnit { + if let Some(chan) = session.channels.get(&channel) { + chan.send(OpenChannelMsg::Msg(ChannelMsg::ExitSignal { + signal_name, + core_dumped, + error_message: error_message.to_string(), + lang_tag: lang_tag.to_string(), + })) + .unwrap_or(()) + } + self.finished(session) + } + + /// Called when the network window is adjusted, meaning that we + /// can send more bytes. This is useful if this client wants to + /// send huge amounts of data, for instance if we have called + /// `Session::data` before, and it returned less than the + /// full amount of data. + #[allow(unused_variables)] + fn window_adjusted( + self, + channel: ChannelId, + mut new_size: u32, + mut session: Session, + ) -> Self::FutureUnit { + if let Some(ref mut enc) = session.common.encrypted { + new_size -= enc.flush_pending(channel) as u32; + } + if let Some(chan) = session.channels.get(&channel) { + chan.send(OpenChannelMsg::Msg(ChannelMsg::WindowAdjusted { new_size })) + .unwrap_or(()) + } + self.finished(session) + } + + /// Called when this client adjusts the network window. Return the + /// next target window and maximum packet size. + #[allow(unused_variables)] + fn adjust_window(&mut self, channel: ChannelId, window: u32) -> u32 { + window + } +} diff --git a/thrussh/src/client/proxy.rs b/thrussh/src/client/proxy.rs new file mode 100644 index 00000000..0f7a2d7a --- /dev/null +++ b/thrussh/src/client/proxy.rs @@ -0,0 +1,77 @@ +use futures::task::{Context, Poll}; +use std::io::{Read, Write}; +use std::net::SocketAddr; +use std::pin::Pin; +use std::process::Command; +use tokio; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::net::TcpStream; + +/// A type to implement either a TCP socket, or proxying through an external command. +pub enum Stream { + #[allow(missing_docs)] + Child(std::process::Child), + #[allow(missing_docs)] + Tcp(TcpStream), +} + +impl Stream { + /// Connect a direct TCP stream (as opposed to a proxied one). + pub async fn tcp_connect(addr: &SocketAddr) -> Result { + TcpStream::connect(addr).await.map(Stream::Tcp) + } + /// Connect through a proxy command. + pub fn proxy_connect(cmd: &str, args: &[&str]) -> Result { + Ok(Stream::Child(Command::new(cmd).args(args).spawn()?)) + } +} + +impl AsyncRead for Stream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut ReadBuf, + ) -> Poll> { + match *self { + Stream::Child(ref mut c) => { + let n = c.stdout.as_mut().unwrap().read(buf.initialize_unfilled())?; + buf.advance(n); + Poll::Ready(Ok(())) + } + Stream::Tcp(ref mut t) => AsyncRead::poll_read(Pin::new(t), cx, buf), + } + } +} + +impl AsyncWrite for Stream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + match *self { + Stream::Child(ref mut c) => Poll::Ready(c.stdin.as_mut().unwrap().write(buf)), + Stream::Tcp(ref mut t) => AsyncWrite::poll_write(Pin::new(t), cx, buf), + } + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context, + ) -> Poll> { + match *self { + Stream::Child(_) => Poll::Ready(Ok(())), + Stream::Tcp(ref mut t) => AsyncWrite::poll_flush(Pin::new(t), cx), + } + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context, + ) -> Poll> { + match *self { + Stream::Child(_) => Poll::Ready(Ok(())), + Stream::Tcp(ref mut t) => AsyncWrite::poll_shutdown(Pin::new(t), cx), + } + } +} diff --git a/thrussh/src/client/session.rs b/thrussh/src/client/session.rs new file mode 100644 index 00000000..fd22a2fa --- /dev/null +++ b/thrussh/src/client/session.rs @@ -0,0 +1,362 @@ +use super::*; + +impl Session { + pub fn channel_open_session(&mut self) -> Result { + let result = if let Some(ref mut enc) = self.common.encrypted { + match enc.state { + EncryptedState::Authenticated => { + let sender_channel = enc.new_channel( + self.common.config.window_size, + self.common.config.maximum_packet_size, + ); + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_OPEN); + enc.write.extend_ssh_string(b"session"); + + // sender channel id. + enc.write.push_u32_be(sender_channel.0); + + // window. + enc.write + .push_u32_be(self.common.config.as_ref().window_size); + + // max packet size. + enc.write + .push_u32_be(self.common.config.as_ref().maximum_packet_size); + }); + sender_channel + } + _ => return Err(Error::NotAuthenticated.into()), + } + } else { + return Err(Error::Inconsistent.into()); + }; + Ok(result) + } + + pub fn channel_open_x11( + &mut self, + originator_address: &str, + originator_port: u32, + ) -> Result { + let result = if let Some(ref mut enc) = self.common.encrypted { + match enc.state { + EncryptedState::Authenticated => { + let sender_channel = enc.new_channel( + self.common.config.window_size, + self.common.config.maximum_packet_size, + ); + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_OPEN); + enc.write.extend_ssh_string(b"x11"); + + // sender channel id. + enc.write.push_u32_be(sender_channel.0); + + // window. + enc.write + .push_u32_be(self.common.config.as_ref().window_size); + + // max packet size. + enc.write + .push_u32_be(self.common.config.as_ref().maximum_packet_size); + + enc.write.extend_ssh_string(originator_address.as_bytes()); + enc.write.push_u32_be(originator_port); // sender channel id. + }); + sender_channel + } + _ => return Err(Error::NotAuthenticated.into()), + } + } else { + return Err(Error::Inconsistent.into()); + }; + Ok(result) + } + + pub fn channel_open_direct_tcpip( + &mut self, + host_to_connect: &str, + port_to_connect: u32, + originator_address: &str, + originator_port: u32, + ) -> Result { + let result = if let Some(ref mut enc) = self.common.encrypted { + match enc.state { + EncryptedState::Authenticated => { + let sender_channel = enc.new_channel( + self.common.config.window_size, + self.common.config.maximum_packet_size, + ); + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_OPEN); + enc.write.extend_ssh_string(b"direct-tcpip"); + + // sender channel id. + enc.write.push_u32_be(sender_channel.0); + + // window. + enc.write + .push_u32_be(self.common.config.as_ref().window_size); + + // max packet size. + enc.write + .push_u32_be(self.common.config.as_ref().maximum_packet_size); + + enc.write.extend_ssh_string(host_to_connect.as_bytes()); + enc.write.push_u32_be(port_to_connect); // sender channel id. + enc.write.extend_ssh_string(originator_address.as_bytes()); + enc.write.push_u32_be(originator_port); // sender channel id. + }); + sender_channel + } + _ => return Err(Error::NotAuthenticated.into()), + } + } else { + return Err(Error::Inconsistent.into()); + }; + Ok(result) + } + + pub fn request_pty( + &mut self, + channel: ChannelId, + want_reply: bool, + term: &str, + col_width: u32, + row_height: u32, + pix_width: u32, + pix_height: u32, + terminal_modes: &[(Pty, u32)], + ) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_REQUEST); + + enc.write.push_u32_be(channel.recipient_channel); + enc.write.extend_ssh_string(b"pty-req"); + enc.write.push(if want_reply { 1 } else { 0 }); + + enc.write.extend_ssh_string(term.as_bytes()); + enc.write.push_u32_be(col_width); + enc.write.push_u32_be(row_height); + enc.write.push_u32_be(pix_width); + enc.write.push_u32_be(pix_height); + + enc.write.push_u32_be((1 + 5 * terminal_modes.len()) as u32); + for &(code, value) in terminal_modes { + enc.write.push(code as u8); + enc.write.push_u32_be(value) + } + // 0 code (to terminate the list) + enc.write.push(0); + }); + } + } + } + + pub fn request_x11( + &mut self, + channel: ChannelId, + want_reply: bool, + single_connection: bool, + x11_authentication_protocol: &str, + x11_authentication_cookie: &str, + x11_screen_number: u32, + ) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_REQUEST); + + enc.write.push_u32_be(channel.recipient_channel); + enc.write.extend_ssh_string(b"x11-req"); + enc.write.push(if want_reply { 1 } else { 0 }); + enc.write.push(if single_connection { 1 } else { 0 }); + enc.write + .extend_ssh_string(x11_authentication_protocol.as_bytes()); + enc.write + .extend_ssh_string(x11_authentication_cookie.as_bytes()); + enc.write.push_u32_be(x11_screen_number); + }); + } + } + } + + pub fn set_env( + &mut self, + channel: ChannelId, + want_reply: bool, + variable_name: &str, + variable_value: &str, + ) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_REQUEST); + + enc.write.push_u32_be(channel.recipient_channel); + enc.write.extend_ssh_string(b"env"); + enc.write.push(if want_reply { 1 } else { 0 }); + enc.write.extend_ssh_string(variable_name.as_bytes()); + enc.write.extend_ssh_string(variable_value.as_bytes()); + }); + } + } + } + + pub fn request_shell(&mut self, want_reply: bool, channel: ChannelId) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_REQUEST); + + enc.write.push_u32_be(channel.recipient_channel); + enc.write.extend_ssh_string(b"shell"); + enc.write.push(if want_reply { 1 } else { 0 }); + }); + } + } + } + + pub fn exec(&mut self, channel: ChannelId, want_reply: bool, command: &str) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_REQUEST); + + enc.write.push_u32_be(channel.recipient_channel); + enc.write.extend_ssh_string(b"exec"); + enc.write.push(if want_reply { 1 } else { 0 }); + enc.write.extend_ssh_string(command.as_bytes()); + }); + return; + } + } + error!("exec"); + } + + pub fn signal(&mut self, channel: ChannelId, signal: Sig) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_REQUEST); + + enc.write.push_u32_be(channel.recipient_channel); + enc.write.extend_ssh_string(b"signal"); + enc.write.push(0); + enc.write.extend_ssh_string(signal.name().as_bytes()); + }); + } + } + } + + pub fn request_subsystem(&mut self, want_reply: bool, channel: ChannelId, name: &str) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_REQUEST); + + enc.write.push_u32_be(channel.recipient_channel); + enc.write.extend_ssh_string(b"subsystem"); + enc.write.push(if want_reply { 1 } else { 0 }); + enc.write.extend_ssh_string(name.as_bytes()); + }); + } + } + } + + pub fn window_change( + &mut self, + channel: ChannelId, + col_width: u32, + row_height: u32, + pix_width: u32, + pix_height: u32, + ) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_REQUEST); + + enc.write.push_u32_be(channel.recipient_channel); + enc.write.extend_ssh_string(b"window-change"); + enc.write.push(0); // this packet never wants reply + enc.write.push_u32_be(col_width); + enc.write.push_u32_be(row_height); + enc.write.push_u32_be(pix_width); + enc.write.push_u32_be(pix_height); + }); + } + } + } + + pub fn tcpip_forward(&mut self, want_reply: bool, address: &str, port: u32) { + if let Some(ref mut enc) = self.common.encrypted { + push_packet!(enc.write, { + enc.write.push(msg::GLOBAL_REQUEST); + enc.write.extend_ssh_string(b"tcpip-forward"); + enc.write.push(if want_reply { 1 } else { 0 }); + enc.write.extend_ssh_string(address.as_bytes()); + enc.write.push_u32_be(port); + }); + } + } + + pub fn cancel_tcpip_forward(&mut self, want_reply: bool, address: &str, port: u32) { + if let Some(ref mut enc) = self.common.encrypted { + push_packet!(enc.write, { + enc.write.push(msg::GLOBAL_REQUEST); + enc.write.extend_ssh_string(b"cancel-tcpip-forward"); + enc.write.push(if want_reply { 1 } else { 0 }); + enc.write.extend_ssh_string(address.as_bytes()); + enc.write.push_u32_be(port); + }); + } + } + + pub fn data(&mut self, channel: ChannelId, data: CryptoVec) { + if let Some(ref mut enc) = self.common.encrypted { + enc.data(channel, data) + } else { + unreachable!() + } + } + + pub fn eof(&mut self, channel: ChannelId) { + if let Some(ref mut enc) = self.common.encrypted { + enc.eof(channel) + } else { + unreachable!() + } + } + + pub fn extended_data(&mut self, channel: ChannelId, ext: u32, data: CryptoVec) { + if let Some(ref mut enc) = self.common.encrypted { + enc.extended_data(channel, ext, data) + } else { + unreachable!() + } + } + + pub fn disconnect(&mut self, reason: Disconnect, description: &str, language_tag: &str) { + self.common.disconnect(reason, description, language_tag); + } + + pub fn has_pending_data(&self, channel: ChannelId) -> bool { + if let Some(ref enc) = self.common.encrypted { + enc.has_pending_data(channel) + } else { + false + } + } + + pub fn sender_window_size(&self, channel: ChannelId) -> usize { + if let Some(ref enc) = self.common.encrypted { + enc.sender_window_size(channel) + } else { + 0 + } + } +} diff --git a/thrussh/src/compression.rs b/thrussh/src/compression.rs new file mode 100644 index 00000000..c07b7a91 --- /dev/null +++ b/thrussh/src/compression.rs @@ -0,0 +1,154 @@ +#[derive(Debug)] +pub enum Compression { + None, + #[cfg(feature = "flate2")] + Zlib, +} + +#[derive(Debug)] +pub enum Compress { + None, + #[cfg(feature = "flate2")] + Zlib(flate2::Compress), +} + +#[derive(Debug)] +pub enum Decompress { + None, + #[cfg(feature = "flate2")] + Zlib(flate2::Decompress), +} + +#[cfg(feature = "flate2")] +impl Compression { + pub fn from_string(s: &str) -> Self { + if s == "zlib" || s == "zlib@openssh.com" { + Compression::Zlib + } else { + Compression::None + } + } + + pub fn init_compress(&self, comp: &mut Compress) { + if let Compression::Zlib = *self { + if let Compress::Zlib(ref mut c) = *comp { + c.reset() + } else { + *comp = Compress::Zlib(flate2::Compress::new(flate2::Compression::fast(), true)) + } + } else { + *comp = Compress::None + } + } + + pub fn init_decompress(&self, comp: &mut Decompress) { + if let Compression::Zlib = *self { + if let Decompress::Zlib(ref mut c) = *comp { + c.reset(true) + } else { + *comp = Decompress::Zlib(flate2::Decompress::new(true)) + } + } else { + *comp = Decompress::None + } + } +} + +#[cfg(not(feature = "flate2"))] +impl Compression { + pub fn from_string(_: &str) -> Self { + Compression::None + } + + pub fn init_compress(&self, _: &mut Compress) {} + + pub fn init_decompress(&self, _: &mut Decompress) {} +} + +#[cfg(not(feature = "flate2"))] +impl Compress { + pub fn compress<'a>( + &mut self, + input: &'a [u8], + _: &'a mut cryptovec::CryptoVec, + ) -> Result<&'a [u8], Error> { + Ok(input) + } +} + +#[cfg(not(feature = "flate2"))] +impl Decompress { + pub fn decompress<'a>( + &mut self, + input: &'a [u8], + _: &'a mut cryptovec::CryptoVec, + ) -> Result<&'a [u8], Error> { + Ok(input) + } +} + +#[cfg(feature = "flate2")] +impl Compress { + pub fn compress<'a>( + &mut self, + input: &'a [u8], + output: &'a mut cryptovec::CryptoVec, + ) -> Result<&'a [u8], crate::Error> { + match *self { + Compress::None => Ok(input), + Compress::Zlib(ref mut z) => { + output.clear(); + let n_in = z.total_in() as usize; + let n_out = z.total_out() as usize; + output.resize(input.len() + 10); + let flush = flate2::FlushCompress::Partial; + loop { + let n_in_ = z.total_in() as usize - n_in; + let n_out_ = z.total_out() as usize - n_out; + let c = z.compress(&input[n_in_..], &mut output[n_out_..], flush)?; + match c { + flate2::Status::BufError => { + output.resize(output.len() * 2); + } + _ => break, + } + } + let n_out_ = z.total_out() as usize - n_out; + Ok(&output[..n_out_]) + } + } + } +} + +#[cfg(feature = "flate2")] +impl Decompress { + pub fn decompress<'a>( + &mut self, + input: &'a [u8], + output: &'a mut cryptovec::CryptoVec, + ) -> Result<&'a [u8], crate::Error> { + match *self { + Decompress::None => Ok(input), + Decompress::Zlib(ref mut z) => { + output.clear(); + let n_in = z.total_in() as usize; + let n_out = z.total_out() as usize; + output.resize(input.len()); + let flush = flate2::FlushDecompress::None; + loop { + let n_in_ = z.total_in() as usize - n_in; + let n_out_ = z.total_out() as usize - n_out; + let d = z.decompress(&input[n_in_..], &mut output[n_out_..], flush); + match d? { + flate2::Status::Ok => { + output.resize(output.len() * 2); + } + _ => break, + } + } + let n_out_ = z.total_out() as usize - n_out; + Ok(&output[..n_out_]) + } + } + } +} diff --git a/thrussh/src/kex.rs b/thrussh/src/kex.rs new file mode 100644 index 00000000..c8d14415 --- /dev/null +++ b/thrussh/src/kex.rs @@ -0,0 +1,227 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +use crate::{cipher, key, msg}; +use byteorder::{BigEndian, ByteOrder}; + +use crate::session::Exchange; +use cryptovec::CryptoVec; +use sodium; +use std::cell::RefCell; +use thrussh_keys::encoding::Encoding; +use rand::RngCore; + +#[doc(hidden)] +pub struct Algorithm { + local_secret: Option, + shared_secret: Option, +} + +impl std::fmt::Debug for Algorithm { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "Algorithm {{ local_secret: [hidden], shared_secret: [hidden] }}", + ) + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct Name(&'static str); +impl AsRef for Name { + fn as_ref(&self) -> &str { + self.0 + } +} +pub const CURVE25519: Name = Name("curve25519-sha256@libssh.org"); + +thread_local! { + static KEY_BUF: RefCell = RefCell::new(CryptoVec::new()); + static BUFFER: RefCell = RefCell::new(CryptoVec::new()); +} + +// We used to support curve "NIST P-256" here, but the security of +// that curve is controversial, see +// http://safecurves.cr.yp.to/rigid.html + +impl Algorithm { + #[doc(hidden)] + pub fn server_dh( + _name: Name, + exchange: &mut Exchange, + payload: &[u8], + ) -> Result { + debug!("server_dh"); + + assert_eq!(payload[0], msg::KEX_ECDH_INIT); + let mut client_pubkey = GroupElement([0; 32]); + { + let pubkey_len = BigEndian::read_u32(&payload[1..]) as usize; + client_pubkey + .0 + .clone_from_slice(&payload[5..(5 + pubkey_len)]) + }; + debug!("client_pubkey: {:?}", client_pubkey); + use sodium::scalarmult::*; + let mut server_secret = Scalar([0; 32]); + rand::thread_rng().fill_bytes(&mut server_secret.0); + let server_pubkey = scalarmult_base(&server_secret); + + // fill exchange. + exchange.server_ephemeral.clear(); + exchange.server_ephemeral.extend(&server_pubkey.0); + let shared = scalarmult(&server_secret, &client_pubkey); + Ok(Algorithm { + local_secret: None, + shared_secret: Some(shared), + }) + } + + #[doc(hidden)] + pub fn client_dh( + _name: Name, + client_ephemeral: &mut CryptoVec, + buf: &mut CryptoVec, + ) -> Result { + use sodium::scalarmult::*; + let mut client_secret = Scalar([0; 32]); + rand::thread_rng().fill_bytes(&mut client_secret.0); + let client_pubkey = scalarmult_base(&client_secret); + + // fill exchange. + client_ephemeral.clear(); + client_ephemeral.extend(&client_pubkey.0); + + buf.push(msg::KEX_ECDH_INIT); + buf.extend_ssh_string(&client_pubkey.0); + + Ok(Algorithm { + local_secret: Some(client_secret), + shared_secret: None, + }) + } + + pub fn compute_shared_secret(&mut self, remote_pubkey_: &[u8]) -> Result<(), crate::Error> { + let local_secret = std::mem::replace(&mut self.local_secret, None).unwrap(); + + use sodium::scalarmult::*; + let mut remote_pubkey = GroupElement([0; 32]); + remote_pubkey.0.clone_from_slice(remote_pubkey_); + let shared = scalarmult(&local_secret, &remote_pubkey); + self.shared_secret = Some(shared); + Ok(()) + } + + pub fn compute_exchange_hash( + &self, + key: &K, + exchange: &Exchange, + buffer: &mut CryptoVec, + ) -> Result { + // Computing the exchange hash, see page 7 of RFC 5656. + buffer.clear(); + buffer.extend_ssh_string(&exchange.client_id); + buffer.extend_ssh_string(&exchange.server_id); + buffer.extend_ssh_string(&exchange.client_kex_init); + buffer.extend_ssh_string(&exchange.server_kex_init); + + key.push_to(buffer); + buffer.extend_ssh_string(&exchange.client_ephemeral); + buffer.extend_ssh_string(&exchange.server_ephemeral); + + if let Some(ref shared) = self.shared_secret { + buffer.extend_ssh_mpint(&shared.0); + } + + use sha2::Digest; + let mut hasher = sha2::Sha256::new(); + hasher.update(&buffer); + Ok(hasher.finalize()) + } + + pub fn compute_keys( + &self, + session_id: &crate::Sha256Hash, + exchange_hash: &crate::Sha256Hash, + cipher: cipher::Name, + is_server: bool, + ) -> Result { + let cipher = match cipher { + super::cipher::chacha20poly1305::NAME => &super::cipher::chacha20poly1305::CIPHER, + _ => unreachable!(), + }; + + // https://tools.ietf.org/html/rfc4253#section-7.2 + BUFFER.with(|buffer| { + KEY_BUF.with(|key| { + let compute_key = |c, key: &mut CryptoVec, len| -> Result<(), crate::Error> { + let mut buffer = buffer.borrow_mut(); + buffer.clear(); + key.clear(); + + if let Some(ref shared) = self.shared_secret { + buffer.extend_ssh_mpint(&shared.0); + } + + buffer.extend(exchange_hash.as_ref()); + buffer.push(c); + buffer.extend(session_id.as_ref()); + let hash = { + use sha2::Digest; + let mut hasher = sha2::Sha256::new(); + hasher.update(&buffer[..]); + hasher.finalize() + }; + key.extend(hash.as_ref()); + + while key.len() < len { + // extend. + buffer.clear(); + if let Some(ref shared) = self.shared_secret { + buffer.extend_ssh_mpint(&shared.0); + } + buffer.extend(exchange_hash.as_ref()); + buffer.extend(key); + let hash = { + use sha2::Digest; + let mut hasher = sha2::Sha256::new(); + hasher.update(&buffer[..]); + hasher.finalize() + }; + key.extend(&hash.as_ref()); + } + Ok(()) + }; + + let (local_to_remote, remote_to_local) = if is_server { + (b'D', b'C') + } else { + (b'C', b'D') + }; + + let mut key = key.borrow_mut(); + compute_key(local_to_remote, &mut key, cipher.key_len)?; + let local_to_remote = (cipher.make_sealing_cipher)(&key); + + compute_key(remote_to_local, &mut key, cipher.key_len)?; + let remote_to_local = (cipher.make_opening_cipher)(&key); + + Ok(super::cipher::CipherPair { + local_to_remote: local_to_remote, + remote_to_local: remote_to_local, + }) + }) + }) + } +} diff --git a/thrussh/src/key.rs b/thrussh/src/key.rs new file mode 100644 index 00000000..b8e2a34f --- /dev/null +++ b/thrussh/src/key.rs @@ -0,0 +1,66 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +use cryptovec::CryptoVec; +use thrussh_keys::encoding::*; +use thrussh_keys::key::*; + +#[doc(hidden)] +pub trait PubKey { + fn push_to(&self, buffer: &mut CryptoVec); +} + +impl PubKey for PublicKey { + fn push_to(&self, buffer: &mut CryptoVec) { + match self { + &PublicKey::Ed25519(ref public) => { + buffer.push_u32_be((ED25519.0.len() + public.key.len() + 8) as u32); + buffer.extend_ssh_string(ED25519.0.as_bytes()); + buffer.extend_ssh_string(&public.key); + } + #[cfg(feature = "openssl")] + &PublicKey::RSA { ref key, .. } => { + let rsa = key.0.rsa().unwrap(); + let e = rsa.e().to_vec(); + let n = rsa.n().to_vec(); + buffer.push_u32_be((4 + SSH_RSA.0.len() + mpint_len(&n) + mpint_len(&e)) as u32); + buffer.extend_ssh_string(SSH_RSA.0.as_bytes()); + buffer.extend_ssh_mpint(&e); + buffer.extend_ssh_mpint(&n); + } + } + } +} + +impl PubKey for KeyPair { + fn push_to(&self, buffer: &mut CryptoVec) { + match self { + &KeyPair::Ed25519(ref key) => { + let public = &key.key[32..]; + buffer.push_u32_be((ED25519.0.len() + public.len() + 8) as u32); + buffer.extend_ssh_string(ED25519.0.as_bytes()); + buffer.extend_ssh_string(public); + } + #[cfg(feature = "openssl")] + &KeyPair::RSA { ref key, .. } => { + let e = key.e().to_vec(); + let n = key.n().to_vec(); + buffer.push_u32_be((4 + SSH_RSA.0.len() + mpint_len(&n) + mpint_len(&e)) as u32); + buffer.extend_ssh_string(SSH_RSA.0.as_bytes()); + buffer.extend_ssh_mpint(&e); + buffer.extend_ssh_mpint(&n); + } + } + } +} diff --git a/thrussh/src/lib.rs b/thrussh/src/lib.rs new file mode 100644 index 00000000..6715b95e --- /dev/null +++ b/thrussh/src/lib.rs @@ -0,0 +1,780 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +//! Server and client SSH asynchronous library, based on tokio/futures. +//! +//! The normal way to use this library, both for clients and for +//! servers, is by creating *handlers*, i.e. types that implement +//! `client::Handler` for clients and `server::Handler` for +//! servers. +//! +//! # Writing servers +//! +//! In the specific case of servers, a server must implement +//! `server::Server`, a trait for creating new `server::Handler`. The +//! main type to look at in the `server` module is `Session` (and +//! `Config`, of course). +//! +//! Here is an example server, which forwards input from each client +//! to all other clients: +//! +//! ``` +//! extern crate thrussh; +//! extern crate thrussh_keys; +//! extern crate futures; +//! extern crate tokio; +//! use std::sync::{Mutex, Arc}; +//! use thrussh::*; +//! use thrussh::server::{Auth, Session}; +//! use thrussh_keys::*; +//! use std::collections::HashMap; +//! use futures::Future; +//! +//! #[tokio::main] +//! async fn main() { +//! let client_key = thrussh_keys::key::KeyPair::generate_ed25519().unwrap(); +//! let client_pubkey = Arc::new(client_key.clone_public_key()); +//! let mut config = thrussh::server::Config::default(); +//! config.connection_timeout = Some(std::time::Duration::from_secs(3)); +//! config.auth_rejection_time = std::time::Duration::from_secs(3); +//! config.keys.push(thrussh_keys::key::KeyPair::generate_ed25519().unwrap()); +//! let config = Arc::new(config); +//! let sh = Server{ +//! client_pubkey, +//! clients: Arc::new(Mutex::new(HashMap::new())), +//! id: 0 +//! }; +//! tokio::time::timeout( +//! std::time::Duration::from_secs(1), +//! thrussh::server::run(config, "0.0.0.0:2222", sh) +//! ).await.unwrap_or(Ok(())); +//! } +//! +//! #[derive(Clone)] +//! struct Server { +//! client_pubkey: Arc, +//! clients: Arc>>, +//! id: usize, +//! } +//! +//! impl server::Server for Server { +//! type Handler = Self; +//! fn new(&mut self, _: Option) -> Self { +//! let s = self.clone(); +//! self.id += 1; +//! s +//! } +//! } +//! +//! impl server::Handler for Server { +//! type Error = anyhow::Error; +//! type FutureAuth = futures::future::Ready>; +//! type FutureUnit = futures::future::Ready>; +//! type FutureBool = futures::future::Ready>; +//! +//! fn finished_auth(mut self, auth: Auth) -> Self::FutureAuth { +//! futures::future::ready(Ok((self, auth))) +//! } +//! fn finished_bool(self, b: bool, s: Session) -> Self::FutureBool { +//! futures::future::ready(Ok((self, s, b))) +//! } +//! fn finished(self, s: Session) -> Self::FutureUnit { +//! futures::future::ready(Ok((self, s))) +//! } +//! fn channel_open_session(self, channel: ChannelId, session: Session) -> Self::FutureUnit { +//! { +//! let mut clients = self.clients.lock().unwrap(); +//! clients.insert((self.id, channel), session.handle()); +//! } +//! self.finished(session) +//! } +//! fn auth_publickey(self, _: &str, _: &key::PublicKey) -> Self::FutureAuth { +//! self.finished_auth(server::Auth::Accept) +//! } +//! fn data(self, channel: ChannelId, data: &[u8], mut session: Session) -> Self::FutureUnit { +//! { +//! let mut clients = self.clients.lock().unwrap(); +//! for ((id, channel), ref mut s) in clients.iter_mut() { +//! if *id != self.id { +//! s.data(*channel, CryptoVec::from_slice(data)); +//! } +//! } +//! } +//! session.data(channel, CryptoVec::from_slice(data)); +//! self.finished(session) +//! } +//! } +//! ``` +//! +//! Note the call to `session.handle()`, which allows to keep a handle +//! to a client outside the event loop. This feature is internally +//! implemented using `futures::sync::mpsc` channels. +//! +//! Note that this is just a toy server. In particular: +//! +//! - It doesn't handle errors when `s.data` returns an error, +//! i.e. when the client has disappeared +//! +//! - Each new connection increments the `id` field. Even though we +//! would need a lot of connections per second for a very long time to +//! saturate it, there are probably better ways to handle this to +//! avoid collisions. +//! +//! +//! # Implementing clients +//! +//! Maybe surprisingly, the data types used by Thrussh to implement +//! clients are relatively more complicated than for servers. This is +//! mostly related to the fact that clients are generally used both in +//! a synchronous way (in the case of SSH, we can think of sending a +//! shell command), and asynchronously (because the server may send +//! unsollicited messages), and hence need to handle multiple +//! interfaces. +//! +//! The important types in the `client` module are `Session` and +//! `Connection`. A `Connection` is typically used to send commands to +//! the server and wait for responses, and contains a `Session`. The +//! `Session` is passed to the `Handler` when the client receives +//! data. +//! +//! ``` +//!extern crate thrussh; +//!extern crate thrussh_keys; +//!extern crate futures; +//!extern crate tokio; +//!extern crate env_logger; +//!use std::sync::Arc; +//!use thrussh::*; +//!use thrussh::server::{Auth, Session}; +//!use thrussh_keys::*; +//!use futures::Future; +//!use std::io::Read; +//! +//! +//!struct Client { +//!} +//! +//!impl client::Handler for Client { +//! type Error = anyhow::Error; +//! type FutureUnit = futures::future::Ready>; +//! type FutureBool = futures::future::Ready>; +//! +//! fn finished_bool(self, b: bool) -> Self::FutureBool { +//! futures::future::ready(Ok((self, b))) +//! } +//! fn finished(self, session: client::Session) -> Self::FutureUnit { +//! futures::future::ready(Ok((self, session))) +//! } +//! fn check_server_key(self, server_public_key: &key::PublicKey) -> Self::FutureBool { +//! println!("check_server_key: {:?}", server_public_key); +//! self.finished_bool(true) +//! } +//! fn channel_open_confirmation(self, channel: ChannelId, max_packet_size: u32, window_size: u32, session: client::Session) -> Self::FutureUnit { +//! println!("channel_open_confirmation: {:?}", channel); +//! self.finished(session) +//! } +//! fn data(self, channel: ChannelId, data: &[u8], session: client::Session) -> Self::FutureUnit { +//! println!("data on channel {:?}: {:?}", channel, std::str::from_utf8(data)); +//! self.finished(session) +//! } +//!} +//! +//! #[tokio::main] +//! async fn main() { +//! let config = thrussh::client::Config::default(); +//! let config = Arc::new(config); +//! let sh = Client{}; +//! +//! let key = thrussh_keys::key::KeyPair::generate_ed25519().unwrap(); +//! let mut agent = thrussh_keys::agent::client::AgentClient::connect_env().await.unwrap(); +//! agent.add_identity(&key, &[]).await.unwrap(); +//! let mut session = thrussh::client::connect(config, "localhost:22", sh).await.unwrap(); +//! if session.authenticate_future(std::env::var("USER").unwrap(), key.clone_public_key(), agent).await.1.unwrap() { +//! let mut channel = session.channel_open_session().await.unwrap(); +//! channel.data(&b"Hello, world!"[..]).await.unwrap(); +//! if let Some(msg) = channel.wait().await { +//! println!("{:?}", msg) +//! } +//! } +//! } +//! ``` +//! # Using non-socket IO / writing tunnels +//! +//! The easy way to implement SSH tunnels, like `ProxyCommand` for +//! OpenSSH, is to use the `thrussh-config` crate, and use the +//! `Stream::tcp_connect` or `Stream::proxy_command` methods of that +//! crate. That crate is a very lightweight layer above Thrussh, only +//! implementing for external commands the traits used for sockets. +//! +//! # The SSH protocol +//! +//! If we exclude the key exchange and authentication phases, handled +//! by Thrussh behind the scenes, the rest of the SSH protocol is +//! relatively simple: clients and servers open *channels*, which are +//! just integers used to handle multiple requests in parallel in a +//! single connection. Once a client has obtained a `ChannelId` by +//! calling one the many `channel_open_…` methods of +//! `client::Connection`, the client may send exec requests and data +//! to the server. +//! +//! A simple client just asking the server to run one command will +//! usually start by calling +//! `client::Connection::channel_open_session`, then +//! `client::Connection::exec`, then possibly +//! `client::Connection::data` a number of times to send data to the +//! command's standard input, and finally `Connection::channel_eof` +//! and `Connection::channel_close`. +//! +//! # Design principles +//! +//! The main goal of this library is conciseness, and reduced size and +//! readability of the library's code. Moreover, this library is split +//! between Thrussh, which implements the main logic of SSH clients +//! and servers, and Thrussh-keys, which implements calls to +//! cryptographic primitives. +//! +//! One non-goal is to implement all possible cryptographic algorithms +//! published since the initial release of SSH. Technical debt is +//! easily acquired, and we would need a very strong reason to go +//! against this principle. If you are designing a system from +//! scratch, we urge you to consider recent cryptographic primitives +//! such as Ed25519 for public key cryptography, and Chacha20-Poly1305 +//! for symmetric cryptography and MAC. +//! +//! # Internal details of the event loop +//! +//! It might seem a little odd that the read/write methods for server +//! or client sessions often return neither `Result` nor +//! `Future`. This is because the data sent to the remote side is +//! buffered, because it needs to be encrypted first, and encryption +//! works on buffers, and for many algorithms, not in place. +//! +//! Hence, the event loop keeps waiting for incoming packets, reacts +//! to them by calling the provided `Handler`, which fills some +//! buffers. If the buffers are non-empty, the event loop then sends +//! them to the socket, flushes the socket, empties the buffers and +//! starts again. In the special case of the server, unsollicited +//! messages sent through a `server::Handle` are processed when there +//! is no incoming packet to read. +//! +#[macro_use] +extern crate bitflags; +#[macro_use] +extern crate log; +extern crate thrussh_libsodium as sodium; +#[macro_use] +extern crate thiserror; + +pub use cryptovec::CryptoVec; +mod auth; +mod cipher; +mod compression; +mod kex; +mod key; +mod msg; +mod negotiation; +mod ssh_read; +mod sshbuffer; + +pub use negotiation::{Named, Preferred}; +mod pty; +pub use pty::Pty; + +macro_rules! push_packet { + ( $buffer:expr, $x:expr ) => {{ + use byteorder::{BigEndian, ByteOrder}; + let i0 = $buffer.len(); + $buffer.extend(b"\0\0\0\0"); + let x = $x; + let i1 = $buffer.len(); + use std::ops::DerefMut; + let buf = $buffer.deref_mut(); + BigEndian::write_u32(&mut buf[i0..], (i1 - i0 - 4) as u32); + x + }}; +} + +type Sha256Hash = generic_array::GenericArray::OutputSize>; + +mod session; + +/// Server side of this library. +pub mod server; + +/// Client side of this library. +pub mod client; + +#[derive(Debug, Error)] +pub enum Error { + /// The key file could not be parsed. + #[error("Could not read key")] + CouldNotReadKey, + + /// Unspecified problem with the beginning of key exchange. + #[error("Key exchange init failed")] + KexInit, + + /// No common key exchange algorithm. + #[error("No common key exchange algorithm")] + NoCommonKexAlgo, + + /// No common signature algorithm. + #[error("No common key algorithm")] + NoCommonKeyAlgo, + + /// No common cipher. + #[error("No common key cipher")] + NoCommonCipher, + + /// No common compression algorithm. + #[error("No common compression algorithm")] + NoCommonCompression, + + /// Invalid SSH version string. + #[error("invalid SSH version string")] + Version, + + /// Error during key exchange. + #[error("Key exchange failed")] + Kex, + + /// Invalid packet authentication code. + #[error("Wrong packet authentication code")] + PacketAuth, + + /// The protocol is in an inconsistent state. + #[error("Inconsistent state of the protocol")] + Inconsistent, + + /// The client is not yet authenticated. + #[error("Not yet authenticated")] + NotAuthenticated, + + /// Index out of bounds. + #[error("Index out of bounds")] + IndexOutOfBounds, + + /// Unknown server key. + #[error("Unknown server key")] + UnknownKey, + + /// The server provided a wrong signature. + #[error("Wrong server signature")] + WrongServerSig, + + /// Message received/sent on unopened channel. + #[error("Channel not open")] + WrongChannel, + + /// Disconnected + #[error("Disconnected")] + Disconnect, + + /// No home directory found when trying to learn new host key. + #[error("No home directory when saving host key")] + NoHomeDir, + + /// Remote key changed, this could mean a man-in-the-middle attack + /// is being performed on the connection. + #[error("Key changed, line {}", line)] + KeyChanged { line: usize }, + + /// Connection closed by the remote side. + #[error("Connection closed by the remote side")] + HUP, + + /// Connection timeout. + #[error("Connection timeout")] + ConnectionTimeout, + + /// Missing authentication method. + #[error("No authentication method")] + NoAuthMethod, + + #[error("Channel send error")] + SendError, + + #[error("Pending buffer limit reached")] + Pending, + + #[error(transparent)] + Keys(#[from] thrussh_keys::Error), + + #[error(transparent)] + IO(#[from] std::io::Error), + + #[error(transparent)] + Utf8(#[from] std::str::Utf8Error), + + #[error(transparent)] + Compress(#[from] flate2::CompressError), + + #[error(transparent)] + Decompress(#[from] flate2::DecompressError), + + #[error(transparent)] + Join(#[from] tokio::task::JoinError), + + #[error(transparent)] + #[cfg(feature = "openssl")] + Openssl(#[from] openssl::error::ErrorStack), + + #[error(transparent)] + Elapsed(#[from] tokio::time::error::Elapsed), +} + +#[derive(Debug, Error)] +#[error("Could not reach the event loop")] +pub struct SendError {} + +/// Since handlers are large, their associated future types must implement this trait to provide reasonable default implementations (basically, rejecting all requests). +pub trait FromFinished: futures::Future> { + /// Turns type `T` into `Self`, a future yielding `T`. + fn finished(t: T) -> Self; +} + +impl FromFinished for futures::future::Ready> { + fn finished(t: T) -> Self { + futures::future::ready(Ok(t)) + } +} + +impl FromFinished for Box> + Unpin> { + fn finished(t: T) -> Self { + Box::new(futures::future::ready(Ok(t))) + } +} + +// mod mac; +// use mac::*; +// mod compression; + +/// The number of bytes read/written, and the number of seconds before a key re-exchange is requested. +#[derive(Debug, Clone)] +pub struct Limits { + pub rekey_write_limit: usize, + pub rekey_read_limit: usize, + pub rekey_time_limit: std::time::Duration, +} + +impl Limits { + /// Create a new `Limits`, checking that the given bounds cannot lead to nonce reuse. + pub fn new(write_limit: usize, read_limit: usize, time_limit: std::time::Duration) -> Limits { + assert!(write_limit <= 1 << 30 && read_limit <= 1 << 30); + Limits { + rekey_write_limit: write_limit, + rekey_read_limit: read_limit, + rekey_time_limit: time_limit, + } + } +} + +impl Default for Limits { + fn default() -> Self { + // Following the recommendations of + // https://tools.ietf.org/html/rfc4253#section-9 + Limits { + rekey_write_limit: 1 << 30, // 1 Gb + rekey_read_limit: 1 << 30, // 1 Gb + rekey_time_limit: std::time::Duration::from_secs(3600), + } + } +} + +pub use auth::{AgentAuthError, MethodSet, Signer}; + +/// A reason for disconnection. +#[allow(missing_docs)] // This should be relatively self-explanatory. +#[derive(Debug)] +pub enum Disconnect { + HostNotAllowedToConnect = 1, + ProtocolError = 2, + KeyExchangeFailed = 3, + #[doc(hidden)] + Reserved = 4, + MACError = 5, + CompressionError = 6, + ServiceNotAvailable = 7, + ProtocolVersionNotSupported = 8, + HostKeyNotVerifiable = 9, + ConnectionLost = 10, + ByApplication = 11, + TooManyConnections = 12, + AuthCancelledByUser = 13, + NoMoreAuthMethodsAvailable = 14, + IllegalUserName = 15, +} + +/// The type of signals that can be sent to a remote process. If you +/// plan to use custom signals, read [the +/// RFC](https://tools.ietf.org/html/rfc4254#section-6.10) to +/// understand the encoding. +#[allow(missing_docs)] +// This should be relatively self-explanatory. +#[derive(Debug, Clone)] +pub enum Sig { + ABRT, + ALRM, + FPE, + HUP, + ILL, + INT, + KILL, + PIPE, + QUIT, + SEGV, + TERM, + USR1, + Custom(String), +} + +impl Sig { + fn name(&self) -> &str { + match *self { + Sig::ABRT => "ABRT", + Sig::ALRM => "ALRM", + Sig::FPE => "FPE", + Sig::HUP => "HUP", + Sig::ILL => "ILL", + Sig::INT => "INT", + Sig::KILL => "KILL", + Sig::PIPE => "PIPE", + Sig::QUIT => "QUIT", + Sig::SEGV => "SEGV", + Sig::TERM => "TERM", + Sig::USR1 => "USR1", + Sig::Custom(ref c) => c, + } + } + fn from_name(name: &[u8]) -> Result { + match name { + b"ABRT" => Ok(Sig::ABRT), + b"ALRM" => Ok(Sig::ALRM), + b"FPE" => Ok(Sig::FPE), + b"HUP" => Ok(Sig::HUP), + b"ILL" => Ok(Sig::ILL), + b"INT" => Ok(Sig::INT), + b"KILL" => Ok(Sig::KILL), + b"PIPE" => Ok(Sig::PIPE), + b"QUIT" => Ok(Sig::QUIT), + b"SEGV" => Ok(Sig::SEGV), + b"TERM" => Ok(Sig::TERM), + b"USR1" => Ok(Sig::USR1), + x => Ok(Sig::Custom(std::str::from_utf8(x)?.to_string())), + } + } +} + +/// Reason for not being able to open a channel. +#[derive(Debug, Copy, Clone, PartialEq)] +#[allow(missing_docs)] +pub enum ChannelOpenFailure { + AdministrativelyProhibited = 1, + ConnectFailed = 2, + UnknownChannelType = 3, + ResourceShortage = 4, +} + +impl ChannelOpenFailure { + fn from_u32(x: u32) -> Option { + match x { + 1 => Some(ChannelOpenFailure::AdministrativelyProhibited), + 2 => Some(ChannelOpenFailure::ConnectFailed), + 3 => Some(ChannelOpenFailure::UnknownChannelType), + 4 => Some(ChannelOpenFailure::ResourceShortage), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// The identifier of a channel. +pub struct ChannelId(u32); + +/// The parameters of a channel. +#[derive(Debug)] +pub(crate) struct Channel { + recipient_channel: u32, + sender_channel: ChannelId, + recipient_window_size: u32, + sender_window_size: u32, + recipient_maximum_packet_size: u32, + sender_maximum_packet_size: u32, + /// Has the other side confirmed the channel? + pub confirmed: bool, + wants_reply: bool, + pending_data: std::collections::VecDeque<(CryptoVec, Option, usize)>, +} + +#[derive(Debug)] +pub enum ChannelMsg { + Data { + data: CryptoVec, + }, + ExtendedData { + data: CryptoVec, + ext: u32, + }, + Eof, + Close, + XonXoff { + client_can_do: bool, + }, + ExitStatus { + exit_status: u32, + }, + ExitSignal { + signal_name: Sig, + core_dumped: bool, + error_message: String, + lang_tag: String, + }, + WindowAdjusted { + new_size: u32, + }, + Success, +} + +#[cfg(test)] +mod test_compress { + use super::server::{Auth, Server as _, Session}; + use super::*; + use std::collections::HashMap; + use std::sync::{Arc, Mutex}; + + #[tokio::test] + async fn compress_local_test() { + let _ = env_logger::try_init(); + + let client_key = thrussh_keys::key::KeyPair::generate_ed25519().unwrap(); + let client_pubkey = Arc::new(client_key.clone_public_key()); + let mut config = server::Config::default(); + config.preferred = Preferred::COMPRESSED; + config.connection_timeout = None; // Some(std::time::Duration::from_secs(3)); + config.auth_rejection_time = std::time::Duration::from_secs(3); + config + .keys + .push(thrussh_keys::key::KeyPair::generate_ed25519().unwrap()); + let config = Arc::new(config); + let mut sh = Server { + client_pubkey, + clients: Arc::new(Mutex::new(HashMap::new())), + id: 0, + }; + + let socket = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = socket.local_addr().unwrap(); + + tokio::spawn(async move { + let (socket, _) = socket.accept().await.unwrap(); + let server = sh.new(socket.peer_addr().ok()); + server::run_stream(config, socket, server).await.unwrap(); + }); + + let mut config = client::Config::default(); + config.preferred = Preferred::COMPRESSED; + let config = Arc::new(config); + + dbg!(&addr); + let mut session = client::connect(config, addr, Client {}).await.unwrap(); + let authenticated = session + .authenticate_publickey(std::env::var("USER").unwrap(), Arc::new(client_key)) + .await + .unwrap(); + assert!(authenticated); + let mut channel = session.channel_open_session().await.unwrap(); + + let data = &b"Hello, world!"[..]; + channel.data(data).await.unwrap(); + let msg = channel.wait().await.unwrap(); + match msg { + ChannelMsg::Data { data: msg_data } => { + assert_eq!(*data, *msg_data) + } + msg => panic!("Unexpected message {:?}", msg), + } + } + + #[derive(Clone)] + struct Server { + client_pubkey: Arc, + clients: Arc>>, + id: usize, + } + + impl server::Server for Server { + type Handler = Self; + fn new(&mut self, _: Option) -> Self { + let s = self.clone(); + self.id += 1; + s + } + } + + impl server::Handler for Server { + type Error = super::Error; + type FutureAuth = futures::future::Ready>; + type FutureUnit = futures::future::Ready>; + type FutureBool = futures::future::Ready>; + + fn finished_auth(self, auth: Auth) -> Self::FutureAuth { + futures::future::ready(Ok((self, auth))) + } + fn finished_bool(self, b: bool, s: Session) -> Self::FutureBool { + futures::future::ready(Ok((self, s, b))) + } + fn finished(self, s: Session) -> Self::FutureUnit { + futures::future::ready(Ok((self, s))) + } + fn channel_open_session(self, channel: ChannelId, session: Session) -> Self::FutureUnit { + { + let mut clients = self.clients.lock().unwrap(); + clients.insert((self.id, channel), session.handle()); + } + self.finished(session) + } + fn auth_publickey(self, _: &str, _: &thrussh_keys::key::PublicKey) -> Self::FutureAuth { + debug!("auth_publickey"); + self.finished_auth(server::Auth::Accept) + } + fn data(self, channel: ChannelId, data: &[u8], mut session: Session) -> Self::FutureUnit { + debug!("server data = {:?}", std::str::from_utf8(data)); + session.data(channel, CryptoVec::from_slice(data)); + self.finished(session) + } + } + + struct Client {} + + impl client::Handler for Client { + type Error = super::Error; + type FutureUnit = futures::future::Ready>; + type FutureBool = futures::future::Ready>; + + fn finished_bool(self, b: bool) -> Self::FutureBool { + futures::future::ready(Ok((self, b))) + } + fn finished(self, session: client::Session) -> Self::FutureUnit { + futures::future::ready(Ok((self, session))) + } + fn check_server_key( + self, + server_public_key: &thrussh_keys::key::PublicKey, + ) -> Self::FutureBool { + println!("check_server_key: {:?}", server_public_key); + self.finished_bool(true) + } + } +} diff --git a/thrussh/src/msg.rs b/thrussh/src/msg.rs new file mode 100644 index 00000000..7b7b56c5 --- /dev/null +++ b/thrussh/src/msg.rs @@ -0,0 +1,59 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// https://tools.ietf.org/html/rfc4253#section-12 +pub const DISCONNECT: u8 = 1; +#[allow(dead_code)] +pub const IGNORE: u8 = 2; +#[allow(dead_code)] +pub const UNIMPLEMENTED: u8 = 3; +#[allow(dead_code)] +pub const DEBUG: u8 = 4; + +pub const SERVICE_REQUEST: u8 = 5; +pub const SERVICE_ACCEPT: u8 = 6; +pub const KEXINIT: u8 = 20; +pub const NEWKEYS: u8 = 21; + +// http://tools.ietf.org/html/rfc5656#section-7.1 +pub const KEX_ECDH_INIT: u8 = 30; +pub const KEX_ECDH_REPLY: u8 = 31; + +// https://tools.ietf.org/html/rfc4250#section-4.1.2 +pub const USERAUTH_REQUEST: u8 = 50; +pub const USERAUTH_FAILURE: u8 = 51; +pub const USERAUTH_SUCCESS: u8 = 52; +pub const USERAUTH_BANNER: u8 = 53; +pub const USERAUTH_PK_OK: u8 = 60; + +// https://tools.ietf.org/html/rfc4256#section-5 +pub const USERAUTH_INFO_REQUEST: u8 = 60; +pub const USERAUTH_INFO_RESPONSE: u8 = 61; + +// https://tools.ietf.org/html/rfc4254#section-9 +pub const GLOBAL_REQUEST: u8 = 80; +pub const REQUEST_SUCCESS: u8 = 81; +pub const REQUEST_FAILURE: u8 = 82; + +pub const CHANNEL_OPEN: u8 = 90; +pub const CHANNEL_OPEN_CONFIRMATION: u8 = 91; +pub const CHANNEL_OPEN_FAILURE: u8 = 92; +pub const CHANNEL_WINDOW_ADJUST: u8 = 93; +pub const CHANNEL_DATA: u8 = 94; +pub const CHANNEL_EXTENDED_DATA: u8 = 95; +pub const CHANNEL_EOF: u8 = 96; +pub const CHANNEL_CLOSE: u8 = 97; +pub const CHANNEL_REQUEST: u8 = 98; +pub const CHANNEL_SUCCESS: u8 = 99; +pub const CHANNEL_FAILURE: u8 = 100; diff --git a/thrussh/src/negotiation.rs b/thrussh/src/negotiation.rs new file mode 100644 index 00000000..368c503f --- /dev/null +++ b/thrussh/src/negotiation.rs @@ -0,0 +1,265 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +use crate::{cipher, kex, msg, Error}; +use std::str::from_utf8; +use thrussh_keys::key; +// use super::mac; // unimplemented +use crate::compression::*; +use cryptovec::CryptoVec; +use thrussh_keys::encoding::{Encoding, Reader}; +use thrussh_keys::key::{KeyPair, PublicKey}; +use rand::RngCore; + +#[derive(Debug)] +pub struct Names { + pub kex: kex::Name, + pub key: key::Name, + pub cipher: cipher::Name, + pub mac: Option<&'static str>, + pub server_compression: Compression, + pub client_compression: Compression, + pub ignore_guessed: bool, +} + +/// Lists of preferred algorithms. This is normally hard-coded into implementations. +#[derive(Debug)] +pub struct Preferred { + /// Preferred key exchange algorithms. + pub kex: &'static [kex::Name], + /// Preferred public key algorithms. + pub key: &'static [key::Name], + /// Preferred symmetric ciphers. + pub cipher: &'static [cipher::Name], + /// Preferred MAC algorithms. + pub mac: &'static [&'static str], + /// Preferred compression algorithms. + pub compression: &'static [&'static str], +} + +impl Preferred { + #[cfg(feature = "openssl")] + pub const DEFAULT: Preferred = Preferred { + kex: &[kex::CURVE25519], + key: &[key::ED25519, key::RSA_SHA2_256, key::RSA_SHA2_512], + cipher: &[cipher::chacha20poly1305::NAME], + mac: &["none"], + compression: &["none", "zlib", "zlib@openssh.com"], + }; + + #[cfg(not(feature = "openssl"))] + pub const DEFAULT: Preferred = Preferred { + kex: &[kex::CURVE25519], + key: &[key::ED25519], + cipher: &[cipher::chacha20poly1305::NAME], + mac: &["none"], + compression: &["none", "zlib", "zlib@openssh.com"], + }; + + pub const COMPRESSED: Preferred = Preferred { + kex: &[kex::CURVE25519], + key: &[key::ED25519, key::RSA_SHA2_256, key::RSA_SHA2_512], + cipher: &[cipher::chacha20poly1305::NAME], + mac: &["none"], + compression: &["zlib", "zlib@openssh.com", "none"], + }; +} + +impl Default for Preferred { + fn default() -> Preferred { + Preferred::DEFAULT + } +} + +/// Named algorithms. +pub trait Named { + /// The name of this algorithm. + fn name(&self) -> &'static str; +} + +impl Named for () { + fn name(&self) -> &'static str { + "" + } +} + +#[cfg(feature = "openssl")] +use thrussh_keys::key::{ED25519, SSH_RSA}; +#[cfg(not(feature = "openssl"))] +use thrussh_keys::key::{ED25519}; + +impl Named for PublicKey { + fn name(&self) -> &'static str { + match self { + &PublicKey::Ed25519(_) => ED25519.0, + #[cfg(feature = "openssl")] + &PublicKey::RSA { .. } => SSH_RSA.0, + } + } +} + +impl Named for KeyPair { + fn name(&self) -> &'static str { + match self { + &KeyPair::Ed25519 { .. } => ED25519.0, + #[cfg(feature = "openssl")] + &KeyPair::RSA { ref hash, .. } => hash.name().0, + } + } +} + +pub trait Select { + fn select + Copy>(a: &[S], b: &[u8]) -> Option<(bool, S)>; + + fn read_kex(buffer: &[u8], pref: &Preferred) -> Result { + let mut r = buffer.reader(17); + let kex_string = r.read_string()?; + let (kex_both_first, kex_algorithm) = if let Some(x) = Self::select(pref.kex, kex_string) { + x + } else { + debug!( + "Could not find common kex algorithm, other side only supports {:?}, we only support {:?}", + from_utf8(kex_string), + pref.kex + ); + return Err(Error::NoCommonKexAlgo.into()); + }; + + let key_string = r.read_string()?; + let (key_both_first, key_algorithm) = if let Some(x) = Self::select(pref.key, key_string) { + x + } else { + debug!( + "Could not find common key algorithm, other side only supports {:?}, we only support {:?}", + from_utf8(key_string), + pref.key + ); + return Err(Error::NoCommonKeyAlgo.into()); + }; + + let cipher_string = r.read_string()?; + let cipher = Self::select(pref.cipher, cipher_string); + if cipher.is_none() { + debug!( + "Could not find common cipher, other side only supports {:?}, we only support {:?}", + from_utf8(cipher_string), + pref.cipher + ); + return Err(Error::NoCommonCipher.into()); + } + r.read_string()?; // cipher server-to-client. + debug!("kex {}", line!()); + let mac = Self::select(pref.mac, r.read_string()?); + let mac = mac.and_then(|(_, x)| Some(x)); + r.read_string()?; // mac server-to-client. + + debug!("kex {}", line!()); + // client-to-server compression. + let client_compression = + if let Some((_, c)) = Self::select(pref.compression, r.read_string()?) { + Compression::from_string(c) + } else { + return Err(Error::NoCommonCompression.into()); + }; + debug!("kex {}", line!()); + // server-to-client compression. + let server_compression = + if let Some((_, c)) = Self::select(pref.compression, r.read_string()?) { + Compression::from_string(c) + } else { + return Err(Error::NoCommonCompression.into()); + }; + debug!("client_compression = {:?}", client_compression); + r.read_string()?; // languages client-to-server + r.read_string()?; // languages server-to-client + + let follows = r.read_byte()? != 0; + match (cipher, mac, follows) { + (Some((_, cipher)), mac, fol) => { + Ok(Names { + kex: kex_algorithm, + key: key_algorithm, + cipher, + mac, + client_compression, + server_compression, + // Ignore the next packet if (1) it follows and (2) it's not the correct guess. + ignore_guessed: fol && !(kex_both_first && key_both_first), + }) + } + _ => Err(Error::KexInit.into()), + } + } +} + +pub struct Server; +pub struct Client; + +impl Select for Server { + fn select + Copy>(server_list: &[S], client_list: &[u8]) -> Option<(bool, S)> { + let mut both_first_choice = true; + for c in client_list.split(|&x| x == b',') { + for &s in server_list { + if c == s.as_ref().as_bytes() { + return Some((both_first_choice, s)); + } + both_first_choice = false + } + } + None + } +} + +impl Select for Client { + fn select + Copy>(client_list: &[S], server_list: &[u8]) -> Option<(bool, S)> { + let mut both_first_choice = true; + for &c in client_list { + for s in server_list.split(|&x| x == b',') { + if s == c.as_ref().as_bytes() { + return Some((both_first_choice, c)); + } + both_first_choice = false + } + } + None + } +} + +pub fn write_kex(prefs: &Preferred, buf: &mut CryptoVec) -> Result<(), Error> { + // buf.clear(); + buf.push(msg::KEXINIT); + + let mut cookie = [0; 16]; + rand::thread_rng().fill_bytes(&mut cookie); + + buf.extend(&cookie); // cookie + buf.extend_list(prefs.kex.iter()); // kex algo + + buf.extend_list(prefs.key.iter()); + + buf.extend_list(prefs.cipher.iter()); // cipher client to server + buf.extend_list(prefs.cipher.iter()); // cipher server to client + + buf.extend_list(prefs.mac.iter()); // mac client to server + buf.extend_list(prefs.mac.iter()); // mac server to client + buf.extend_list(prefs.compression.iter()); // compress client to server + buf.extend_list(prefs.compression.iter()); // compress server to client + + buf.write_empty_list(); // languages client to server + buf.write_empty_list(); // languagesserver to client + + buf.push(0); // doesn't follow + buf.extend(&[0, 0, 0, 0]); // reserved + Ok(()) +} diff --git a/thrussh/src/pty.rs b/thrussh/src/pty.rs new file mode 100755 index 00000000..f9224a9d --- /dev/null +++ b/thrussh/src/pty.rs @@ -0,0 +1,132 @@ +#[allow(non_camel_case_types, missing_docs)] +#[derive(Debug, Copy, Clone, PartialEq)] +/// Standard pseudo-terminal codes. +pub enum Pty { + TTY_OP_END = 0, + VINTR = 1, + VQUIT = 2, + VERASE = 3, + VKILL = 4, + VEOF = 5, + VEOL = 6, + VEOL2 = 7, + VSTART = 8, + VSTOP = 9, + VSUSP = 10, + VDSUSP = 11, + + VREPRINT = 12, + VWERASE = 13, + VLNEXT = 14, + VFLUSH = 15, + VSWTCH = 16, + VSTATUS = 17, + VDISCARD = 18, + IGNPAR = 30, + PARMRK = 31, + INPCK = 32, + ISTRIP = 33, + INLCR = 34, + IGNCR = 35, + ICRNL = 36, + IUCLC = 37, + IXON = 38, + IXANY = 39, + IXOFF = 40, + IMAXBEL = 41, + ISIG = 50, + ICANON = 51, + XCASE = 52, + ECHO = 53, + ECHOE = 54, + ECHOK = 55, + ECHONL = 56, + NOFLSH = 57, + TOSTOP = 58, + IEXTEN = 59, + ECHOCTL = 60, + ECHOKE = 61, + PENDIN = 62, + OPOST = 70, + OLCUC = 71, + ONLCR = 72, + OCRNL = 73, + ONOCR = 74, + ONLRET = 75, + + CS7 = 90, + CS8 = 91, + PARENB = 92, + PARODD = 93, + + TTY_OP_ISPEED = 128, + TTY_OP_OSPEED = 129, +} + +impl Pty { + #[doc(hidden)] + pub fn from_u8(x: u8) -> Option { + match x { + 0 => None, + 1 => Some(Pty::VINTR), + 2 => Some(Pty::VQUIT), + 3 => Some(Pty::VERASE), + 4 => Some(Pty::VKILL), + 5 => Some(Pty::VEOF), + 6 => Some(Pty::VEOL), + 7 => Some(Pty::VEOL2), + 8 => Some(Pty::VSTART), + 9 => Some(Pty::VSTOP), + 10 => Some(Pty::VSUSP), + 11 => Some(Pty::VDSUSP), + + 12 => Some(Pty::VREPRINT), + 13 => Some(Pty::VWERASE), + 14 => Some(Pty::VLNEXT), + 15 => Some(Pty::VFLUSH), + 16 => Some(Pty::VSWTCH), + 17 => Some(Pty::VSTATUS), + 18 => Some(Pty::VDISCARD), + 30 => Some(Pty::IGNPAR), + 31 => Some(Pty::PARMRK), + 32 => Some(Pty::INPCK), + 33 => Some(Pty::ISTRIP), + 34 => Some(Pty::INLCR), + 35 => Some(Pty::IGNCR), + 36 => Some(Pty::ICRNL), + 37 => Some(Pty::IUCLC), + 38 => Some(Pty::IXON), + 39 => Some(Pty::IXANY), + 40 => Some(Pty::IXOFF), + 41 => Some(Pty::IMAXBEL), + 50 => Some(Pty::ISIG), + 51 => Some(Pty::ICANON), + 52 => Some(Pty::XCASE), + 53 => Some(Pty::ECHO), + 54 => Some(Pty::ECHOE), + 55 => Some(Pty::ECHOK), + 56 => Some(Pty::ECHONL), + 57 => Some(Pty::NOFLSH), + 58 => Some(Pty::TOSTOP), + 59 => Some(Pty::IEXTEN), + 60 => Some(Pty::ECHOCTL), + 61 => Some(Pty::ECHOKE), + 62 => Some(Pty::PENDIN), + 70 => Some(Pty::OPOST), + 71 => Some(Pty::OLCUC), + 72 => Some(Pty::ONLCR), + 73 => Some(Pty::OCRNL), + 74 => Some(Pty::ONOCR), + 75 => Some(Pty::ONLRET), + + 90 => Some(Pty::CS7), + 91 => Some(Pty::CS8), + 92 => Some(Pty::PARENB), + 93 => Some(Pty::PARODD), + + 128 => Some(Pty::TTY_OP_ISPEED), + 129 => Some(Pty::TTY_OP_OSPEED), + _ => None, + } + } +} diff --git a/thrussh/src/server/encrypted.rs b/thrussh/src/server/encrypted.rs new file mode 100644 index 00000000..ad44b55c --- /dev/null +++ b/thrussh/src/server/encrypted.rs @@ -0,0 +1,906 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +use super::super::*; +use super::*; +use auth::*; +use byteorder::{BigEndian, ByteOrder}; +use msg; +use negotiation; +use negotiation::Select; +use std::cell::RefCell; +use thrussh_keys::encoding::{Encoding, Position, Reader}; +use thrussh_keys::key; +use thrussh_keys::key::Verify; +use tokio::time::Instant; + +impl Session { + /// Returns false iff a request was rejected. + pub(in crate) async fn server_read_encrypted( + mut self, + handler: &mut Option, + buf: &[u8], + ) -> Result { + debug!( + "server_read_encrypted, buf = {:?}", + &buf[..buf.len().min(20)] + ); + // Either this packet is a KEXINIT, in which case we start a key re-exchange. + + let mut enc = self.common.encrypted.as_mut().unwrap(); + if buf[0] == msg::KEXINIT { + debug!("Received rekeying request"); + // If we're not currently rekeying, but `buf` is a rekey request + if let Some(Kex::KexInit(kexinit)) = enc.rekey.take() { + enc.rekey = Some(kexinit.server_parse( + self.common.config.as_ref(), + &self.common.cipher, + buf, + &mut self.common.write_buffer, + )?); + } else if let Some(exchange) = enc.exchange.take() { + let kexinit = KexInit::received_rekey( + exchange, + negotiation::Server::read_kex(buf, &self.common.config.as_ref().preferred)?, + &enc.session_id, + ); + enc.rekey = Some(kexinit.server_parse( + self.common.config.as_ref(), + &mut self.common.cipher, + buf, + &mut self.common.write_buffer, + )?); + } + self.flush()?; + return Ok(self); + } + + match enc.rekey.take() { + Some(Kex::KexDh(kexdh)) => { + enc.rekey = Some(kexdh.parse( + self.common.config.as_ref(), + &self.common.cipher, + buf, + &mut self.common.write_buffer, + )?); + self.flush()?; + return Ok(self); + } + Some(Kex::NewKeys(newkeys)) => { + if buf[0] != msg::NEWKEYS { + return Err(Error::Kex.into()); + } + self.common.write_buffer.bytes = 0; + enc.last_rekey = std::time::Instant::now(); + + // Ok, NEWKEYS received, now encrypted. + enc.flush_all_pending(); + let mut pending = std::mem::replace(&mut self.pending_reads, Vec::new()); + for p in pending.drain(..) { + self = self.process_packet(handler, &p).await? + } + self.pending_reads = pending; + self.pending_len = 0; + self.common.newkeys(newkeys); + self.flush()?; + return Ok(self); + } + Some(Kex::KexInit(k)) => { + enc.rekey = Some(Kex::KexInit(k)); + self.pending_len += buf.len() as u32; + if self.pending_len > 2 * self.target_window_size { + return Err(Error::Pending.into()) + } + self.pending_reads.push(CryptoVec::from_slice(buf)); + return Ok(self); + } + rek => { + debug!("rek = {:?}", rek); + enc.rekey = rek + }, + } + self.process_packet(handler, buf).await + } + + async fn process_packet( + mut self, + handler: &mut Option, + buf: &[u8], + ) -> Result { + let instant = tokio::time::Instant::now() + self.common.config.auth_rejection_time; + let mut enc = self.common.encrypted.as_mut().unwrap(); + // If we've successfully read a packet. + match enc.state { + EncryptedState::WaitingServiceRequest { + ref mut accepted, .. + } if buf[0] == msg::SERVICE_REQUEST => { + let mut r = buf.reader(1); + let request = r.read_string().map_err(crate::Error::from)?; + debug!("request: {:?}", std::str::from_utf8(request)); + if request == b"ssh-userauth" { + let auth_request = server_accept_service( + self.common.config.as_ref().auth_banner, + self.common.config.as_ref().methods, + &mut enc.write, + ); + *accepted = true; + enc.state = EncryptedState::WaitingAuthRequest(auth_request); + } + Ok(self) + } + EncryptedState::WaitingAuthRequest(_) if buf[0] == msg::USERAUTH_REQUEST => { + enc.server_read_auth_request(instant, handler, buf, &mut self.common.auth_user) + .await?; + if let EncryptedState::InitCompression = enc.state { + enc.client_compression.init_decompress(&mut enc.decompress); + } + Ok(self) + } + EncryptedState::WaitingAuthRequest(ref mut auth) + if buf[0] == msg::USERAUTH_INFO_RESPONSE => + { + if read_userauth_info_response( + instant, + handler, + &mut enc.write, + auth, + &mut self.common.auth_user, + buf, + ) + .await? + { + if let EncryptedState::InitCompression = enc.state { + enc.client_compression.init_decompress(&mut enc.decompress); + } + } + Ok(self) + } + EncryptedState::InitCompression => { + enc.server_compression.init_compress(&mut enc.compress); + enc.state = EncryptedState::Authenticated; + self.server_read_authenticated(handler, buf).await + } + EncryptedState::Authenticated => self.server_read_authenticated(handler, buf).await, + _ => Ok(self), + } + } +} + +fn server_accept_service( + banner: Option<&str>, + methods: MethodSet, + buffer: &mut CryptoVec, +) -> AuthRequest { + push_packet!(buffer, { + buffer.push(msg::SERVICE_ACCEPT); + buffer.extend_ssh_string(b"ssh-userauth"); + }); + + if let Some(ref banner) = banner { + push_packet!(buffer, { + buffer.push(msg::USERAUTH_BANNER); + buffer.extend_ssh_string(banner.as_bytes()); + buffer.extend_ssh_string(b""); + }) + } + + AuthRequest { + methods: methods, + partial_success: false, // not used immediately anway. + current: None, + rejection_count: 0, + } +} + +impl Encrypted { + /// Returns false iff the request was rejected. + async fn server_read_auth_request( + &mut self, + until: Instant, + handler: &mut Option, + buf: &[u8], + auth_user: &mut String, + ) -> Result<(), H::Error> { + // https://tools.ietf.org/html/rfc4252#section-5 + let mut r = buf.reader(1); + let user = r.read_string().map_err(crate::Error::from)?; + let user = std::str::from_utf8(user).map_err(crate::Error::from)?; + let service_name = r.read_string().map_err(crate::Error::from)?; + let method = r.read_string().map_err(crate::Error::from)?; + debug!( + "name: {:?} {:?} {:?}", + user, + std::str::from_utf8(service_name), + std::str::from_utf8(method) + ); + + if service_name == b"ssh-connection" { + if method == b"password" { + let auth_request = if let EncryptedState::WaitingAuthRequest(ref mut a) = self.state + { + a + } else { + unreachable!() + }; + auth_user.clear(); + auth_user.push_str(user); + r.read_byte().map_err(crate::Error::from)?; + let password = r.read_string().map_err(crate::Error::from)?; + let password = std::str::from_utf8(password).map_err(crate::Error::from)?; + let handler_ = handler.take().unwrap(); + let (handler_, auth) = handler_.auth_password(user, password).await?; + *handler = Some(handler_); + if let Auth::Accept = auth { + server_auth_request_success(&mut self.write); + self.state = EncryptedState::InitCompression; + } else { + auth_user.clear(); + auth_request.methods = auth_request.methods - MethodSet::PASSWORD; + auth_request.partial_success = false; + reject_auth_request(until, &mut self.write, auth_request).await; + } + Ok(()) + } else if method == b"publickey" { + self.server_read_auth_request_pk(until, handler, buf, auth_user, user, r) + .await + } else if method == b"keyboard-interactive" { + let auth_request = if let EncryptedState::WaitingAuthRequest(ref mut a) = self.state + { + a + } else { + unreachable!() + }; + auth_user.clear(); + auth_user.push_str(user); + let _ = r.read_string().map_err(crate::Error::from)?; // language_tag, deprecated. + let submethods = std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + debug!("{:?}", submethods); + auth_request.current = Some(CurrentRequest::KeyboardInteractive { + submethods: submethods.to_string(), + }); + let h = handler.take().unwrap(); + let (h, auth) = h.auth_keyboard_interactive(user, submethods, None).await?; + *handler = Some(h); + if reply_userauth_info_response(until, auth_request, &mut self.write, auth).await? { + self.state = EncryptedState::InitCompression + } + Ok(()) + } else { + // Other methods of the base specification are insecure or optional. + let auth_request = if let EncryptedState::WaitingAuthRequest(ref mut a) = self.state + { + a + } else { + unreachable!() + }; + reject_auth_request(until, &mut self.write, auth_request).await; + Ok(()) + } + } else { + // Unknown service + Err(Error::Inconsistent.into()) + } + } +} + +thread_local! { + static SIGNATURE_BUFFER: RefCell = RefCell::new(CryptoVec::new()); +} + +impl Encrypted { + async fn server_read_auth_request_pk<'a, H: Handler>( + &mut self, + until: Instant, + handler: &mut Option, + buf: &[u8], + auth_user: &mut String, + user: &str, + mut r: Position<'a>, + ) -> Result<(), H::Error> { + let auth_request = if let EncryptedState::WaitingAuthRequest(ref mut a) = self.state { + a + } else { + unreachable!() + }; + let is_real = r.read_byte().map_err(crate::Error::from)?; + let pubkey_algo = r.read_string().map_err(crate::Error::from)?; + let pubkey_key = r.read_string().map_err(crate::Error::from)?; + debug!("algo: {:?}, key: {:?}", pubkey_algo, pubkey_key); + match key::PublicKey::parse(pubkey_algo, pubkey_key) { + Ok(mut pubkey) => { + debug!("is_real = {:?}", is_real); + + if is_real != 0 { + let pos0 = r.position; + let sent_pk_ok = if let Some(CurrentRequest::PublicKey { sent_pk_ok, .. }) = + auth_request.current + { + sent_pk_ok + } else { + false + }; + + let signature = r.read_string().map_err(crate::Error::from)?; + debug!("signature = {:?}", signature); + let mut s = signature.reader(0); + let algo_ = s.read_string().map_err(crate::Error::from)?; + pubkey.set_algorithm(algo_); + debug!("algo_: {:?}", algo_); + let sig = s.read_string().map_err(crate::Error::from)?; + let init = &buf[0..pos0]; + + let is_valid = if sent_pk_ok && user == auth_user { + true + } else if auth_user.len() == 0 { + auth_user.clear(); + auth_user.push_str(user); + let h = handler.take().unwrap(); + let (h, auth) = h.auth_publickey(user, &pubkey).await?; + *handler = Some(h); + auth == Auth::Accept + } else { + false + }; + if is_valid { + let session_id = self.session_id.as_ref(); + if SIGNATURE_BUFFER.with(|buf| { + let mut buf = buf.borrow_mut(); + buf.clear(); + buf.extend_ssh_string(session_id); + buf.extend(init); + // Verify signature. + pubkey.verify_client_auth(&buf, sig) + }) { + debug!("signature verified"); + server_auth_request_success(&mut self.write); + self.state = EncryptedState::InitCompression; + } else { + debug!("signature wrong"); + reject_auth_request(until, &mut self.write, auth_request).await; + } + } else { + reject_auth_request(until, &mut self.write, auth_request).await; + } + Ok(()) + } else { + auth_user.clear(); + auth_user.push_str(user); + let h = handler.take().unwrap(); + let (h, auth) = h.auth_publickey(user, &pubkey).await?; + *handler = Some(h); + if auth == Auth::Accept { + let mut public_key = CryptoVec::new(); + public_key.extend(pubkey_key); + + let mut algo = CryptoVec::new(); + algo.extend(pubkey_algo); + debug!("pubkey_key: {:?}", pubkey_key); + push_packet!(self.write, { + self.write.push(msg::USERAUTH_PK_OK); + self.write.extend_ssh_string(&pubkey_algo); + self.write.extend_ssh_string(&pubkey_key); + }); + + auth_request.current = Some(CurrentRequest::PublicKey { + key: public_key, + algo: algo, + sent_pk_ok: true, + }); + } else { + debug!("signature wrong"); + auth_request.partial_success = false; + auth_user.clear(); + reject_auth_request(until, &mut self.write, auth_request).await; + } + Ok(()) + } + } + Err(e) => { + if let thrussh_keys::Error::CouldNotReadKey = e { + reject_auth_request(until, &mut self.write, auth_request).await; + Ok(()) + } else { + Err(crate::Error::from(e).into()) + } + } + } + } +} + +async fn reject_auth_request( + until: Instant, + write: &mut CryptoVec, + auth_request: &mut AuthRequest, +) { + debug!("rejecting {:?}", auth_request); + push_packet!(write, { + write.push(msg::USERAUTH_FAILURE); + write.extend_list(auth_request.methods); + write.push(if auth_request.partial_success { 1 } else { 0 }); + }); + auth_request.current = None; + auth_request.rejection_count += 1; + debug!("packet pushed"); + tokio::time::sleep_until(until).await +} + +fn server_auth_request_success(buffer: &mut CryptoVec) { + push_packet!(buffer, { + buffer.push(msg::USERAUTH_SUCCESS); + }) +} + +async fn read_userauth_info_response( + until: Instant, + handler: &mut Option, + write: &mut CryptoVec, + auth_request: &mut AuthRequest, + user: &mut String, + b: &[u8], +) -> Result { + if let Some(CurrentRequest::KeyboardInteractive { ref submethods }) = auth_request.current { + let mut r = b.reader(1); + let n = r.read_u32().map_err(crate::Error::from)?; + let response = Response { pos: r, n: n }; + let h = handler.take().unwrap(); + let (h, auth) = h + .auth_keyboard_interactive(user, submethods, Some(response)) + .await?; + *handler = Some(h); + reply_userauth_info_response(until, auth_request, write, auth) + .await + .map_err(|e| H::Error::from(crate::Error::from(e))) + } else { + reject_auth_request(until, write, auth_request).await; + Ok(false) + } +} + +async fn reply_userauth_info_response( + until: Instant, + auth_request: &mut AuthRequest, + write: &mut CryptoVec, + auth: Auth, +) -> Result { + match auth { + Auth::Accept => { + server_auth_request_success(write); + Ok(true) + } + Auth::Reject => { + auth_request.partial_success = false; + reject_auth_request(until, write, auth_request).await; + Ok(false) + } + Auth::Partial { + name, + instructions, + prompts, + } => { + push_packet!(write, { + write.push(msg::USERAUTH_INFO_REQUEST); + write.extend_ssh_string(name.as_bytes()); + write.extend_ssh_string(instructions.as_bytes()); + write.extend_ssh_string(b""); // lang, should be empty + write.push_u32_be(prompts.len() as u32); + for &(ref a, b) in prompts.iter() { + write.extend_ssh_string(a.as_bytes()); + write.push(if b { 1 } else { 0 }); + } + }); + Ok(false) + } + Auth::UnsupportedMethod => unreachable!(), + } +} + +impl Session { + async fn server_read_authenticated( + mut self, + handler: &mut Option, + buf: &[u8], + ) -> Result { + debug!( + "authenticated buf = {:?}", + &buf[..std::cmp::min(buf.len(), 100)] + ); + match buf[0] { + msg::CHANNEL_OPEN => self.server_handle_channel_open(handler, buf).await, + msg::CHANNEL_CLOSE => { + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + if let Some(ref mut enc) = self.common.encrypted { + enc.channels.remove(&channel_num); + } + debug!("handler.channel_close {:?}", channel_num); + let h = handler.take().unwrap(); + let (h, s) = h.channel_close(channel_num, self).await?; + *handler = Some(h); + Ok(s) + } + msg::CHANNEL_EOF => { + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + debug!("handler.channel_eof {:?}", channel_num); + let h = handler.take().unwrap(); + let (h, s) = h.channel_eof(channel_num, self).await?; + *handler = Some(h); + Ok(s) + } + msg::CHANNEL_EXTENDED_DATA | msg::CHANNEL_DATA => { + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + + let ext = if buf[0] == msg::CHANNEL_DATA { + None + } else { + Some(r.read_u32().map_err(crate::Error::from)?) + }; + debug!("handler.data {:?} {:?}", ext, channel_num); + let data = r.read_string().map_err(crate::Error::from)?; + let target = self.target_window_size; + + let mut h = handler.take().unwrap(); + if let Some(ref mut enc) = self.common.encrypted { + if enc.adjust_window_size(channel_num, data, target) { + let window = h.adjust_window(channel_num, self.target_window_size); + if window > 0 { + self.target_window_size = window + } + } + } + self.flush()?; + let (h, s) = if let Some(ext) = ext { + h.extended_data(channel_num, ext, &data, self).await? + } else { + h.data(channel_num, &data, self).await? + }; + *handler = Some(h); + Ok(s) + } + + msg::CHANNEL_WINDOW_ADJUST => { + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + let amount = r.read_u32().map_err(crate::Error::from)?; + let mut new_value = 0; + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get_mut(&channel_num) { + channel.recipient_window_size += amount; + new_value = channel.recipient_window_size; + } else { + return Err(Error::WrongChannel.into()); + } + } + debug!("handler.window_adjusted {:?}", channel_num); + let h = handler.take().unwrap(); + let (h, s) = h + .window_adjusted(channel_num, new_value as usize, self) + .await?; + *handler = Some(h); + Ok(s) + } + + msg::CHANNEL_REQUEST => { + let mut r = buf.reader(1); + let channel_num = ChannelId(r.read_u32().map_err(crate::Error::from)?); + let req_type = r.read_string().map_err(crate::Error::from)?; + let wants_reply = r.read_byte().map_err(crate::Error::from)?; + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get_mut(&channel_num) { + channel.wants_reply = wants_reply != 0; + } + } + match req_type { + b"pty-req" => { + let term = + std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let col_width = r.read_u32().map_err(crate::Error::from)?; + let row_height = r.read_u32().map_err(crate::Error::from)?; + let pix_width = r.read_u32().map_err(crate::Error::from)?; + let pix_height = r.read_u32().map_err(crate::Error::from)?; + let mut modes = [(Pty::TTY_OP_END, 0); 130]; + let mut i = 0; + { + let mode_string = r.read_string().map_err(crate::Error::from)?; + while 5 * i < mode_string.len() { + let code = mode_string[5 * i]; + if code == 0 { + break; + } + let num = BigEndian::read_u32(&mode_string[5 * i + 1..]); + debug!("code = {:?}", code); + if let Some(code) = Pty::from_u8(code) { + modes[i] = (code, num); + } else { + info!("pty-req: unknown pty code {:?}", code); + } + i += 1 + } + } + debug!("handler.pty_request {:?}", channel_num); + let h = handler.take().unwrap(); + let (h, s) = h + .pty_request( + channel_num, + term, + col_width, + row_height, + pix_width, + pix_height, + &modes[0..i], + self, + ) + .await?; + *handler = Some(h); + Ok(s) + } + b"x11-req" => { + let single_connection = r.read_byte().map_err(crate::Error::from)? != 0; + let x11_auth_protocol = + std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let x11_auth_cookie = + std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let x11_screen_number = r.read_u32().map_err(crate::Error::from)?; + debug!("handler.x11_request {:?}", channel_num); + let h = handler.take().unwrap(); + let (h, s) = h + .x11_request( + channel_num, + single_connection, + x11_auth_protocol, + x11_auth_cookie, + x11_screen_number, + self, + ) + .await?; + *handler = Some(h); + Ok(s) + } + b"env" => { + let env_variable = + std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let env_value = + std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + debug!("handler.env_request {:?}", channel_num); + let h = handler.take().unwrap(); + let (h, s) = h + .env_request(channel_num, env_variable, env_value, self) + .await?; + *handler = Some(h); + Ok(s) + } + b"shell" => { + debug!("handler.shell_request {:?}", channel_num); + let h = handler.take().unwrap(); + let (h, s) = h.shell_request(channel_num, self).await?; + *handler = Some(h); + Ok(s) + } + b"exec" => { + let req = r.read_string().map_err(crate::Error::from)?; + debug!("handler.exec_request {:?}", channel_num); + let h = handler.take().unwrap(); + let (h, s) = h.exec_request(channel_num, req, self).await?; + *handler = Some(h); + debug!("executed"); + Ok(s) + } + b"subsystem" => { + let name = + std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + debug!("handler.subsystem_request {:?}", channel_num); + let h = handler.take().unwrap(); + let (h, s) = h.subsystem_request(channel_num, name, self).await?; + *handler = Some(h); + Ok(s) + } + b"window-change" => { + let col_width = r.read_u32().map_err(crate::Error::from)?; + let row_height = r.read_u32().map_err(crate::Error::from)?; + let pix_width = r.read_u32().map_err(crate::Error::from)?; + let pix_height = r.read_u32().map_err(crate::Error::from)?; + debug!("handler.window_change {:?}", channel_num); + let h = handler.take().unwrap(); + let (h, s) = h + .window_change_request( + channel_num, + col_width, + row_height, + pix_width, + pix_height, + self, + ) + .await?; + *handler = Some(h); + Ok(s) + } + b"signal" => { + let signal_name = + Sig::from_name(r.read_string().map_err(crate::Error::from)?)?; + debug!("handler.signal {:?} {:?}", channel_num, signal_name); + let h = handler.take().unwrap(); + let (h, s) = h.signal(channel_num, signal_name, self).await?; + *handler = Some(h); + Ok(s) + } + x => { + warn!("unknown channel request {}", String::from_utf8_lossy(x)); + self.channel_failure(channel_num); + Ok(self) + } + } + } + msg::GLOBAL_REQUEST => { + let mut r = buf.reader(1); + let req_type = r.read_string().map_err(crate::Error::from)?; + self.common.wants_reply = r.read_byte().map_err(crate::Error::from)? != 0; + match req_type { + b"tcpip-forward" => { + let address = + std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let port = r.read_u32().map_err(crate::Error::from)?; + debug!("handler.tcpip_forward {:?} {:?}", address, port); + let h = handler.take().unwrap(); + let (h, mut s, result) = h.tcpip_forward(address, port, self).await?; + *handler = Some(h); + if let Some(ref mut enc) = s.common.encrypted { + if result { + push_packet!(enc.write, enc.write.push(msg::REQUEST_SUCCESS)) + } else { + push_packet!(enc.write, enc.write.push(msg::REQUEST_FAILURE)) + } + } + Ok(s) + } + b"cancel-tcpip-forward" => { + let address = + std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let port = r.read_u32().map_err(crate::Error::from)?; + debug!("handler.cancel_tcpip_forward {:?} {:?}", address, port); + let h = handler.take().unwrap(); + let (h, mut s, result) = + h.cancel_tcpip_forward(address, port, self).await?; + *handler = Some(h); + if let Some(ref mut enc) = s.common.encrypted { + if result { + push_packet!(enc.write, enc.write.push(msg::REQUEST_SUCCESS)) + } else { + push_packet!(enc.write, enc.write.push(msg::REQUEST_FAILURE)) + } + } + Ok(s) + } + _ => { + if let Some(ref mut enc) = self.common.encrypted { + push_packet!(enc.write, { + enc.write.push(msg::REQUEST_FAILURE); + }); + } + Ok(self) + } + } + } + m => { + debug!("unknown message received: {:?}", m); + Ok(self) + } + } + } + + async fn server_handle_channel_open( + mut self, + handler: &mut Option, + buf: &[u8], + ) -> Result { + // https://tools.ietf.org/html/rfc4254#section-5.1 + let mut r = buf.reader(1); + let typ = r.read_string().map_err(crate::Error::from)?; + let sender = r.read_u32().map_err(crate::Error::from)?; + let window = r.read_u32().map_err(crate::Error::from)?; + let maxpacket = r.read_u32().map_err(crate::Error::from)?; + + let sender_channel = if let Some(ref mut enc) = self.common.encrypted { + enc.new_channel_id() + } else { + unreachable!() + }; + let channel = Channel { + recipient_channel: sender, + + // "sender" is the local end, i.e. we're the sender, the remote is the recipient. + sender_channel: sender_channel, + + recipient_window_size: window, + sender_window_size: self.common.config.window_size, + recipient_maximum_packet_size: maxpacket, + sender_maximum_packet_size: self.common.config.maximum_packet_size, + confirmed: true, + wants_reply: false, + pending_data: std::collections::VecDeque::new(), + }; + match typ { + b"session" => { + self.confirm_channel_open(channel); + let h = handler.take().unwrap(); + let (h, s) = h.channel_open_session(sender_channel, self).await?; + *handler = Some(h); + Ok(s) + } + b"x11" => { + self.confirm_channel_open(channel); + let a = std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let b = r.read_u32().map_err(crate::Error::from)?; + let h = handler.take().unwrap(); + let (h, s) = h.channel_open_x11(sender_channel, a, b, self).await?; + *handler = Some(h); + Ok(s) + } + b"direct-tcpip" => { + self.confirm_channel_open(channel); + let a = std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let b = r.read_u32().map_err(crate::Error::from)?; + let c = std::str::from_utf8(r.read_string().map_err(crate::Error::from)?) + .map_err(crate::Error::from)?; + let d = r.read_u32().map_err(crate::Error::from)?; + let h = handler.take().unwrap(); + let (h, s) = h + .channel_open_direct_tcpip(sender_channel, a, b, c, d, self) + .await?; + *handler = Some(h); + Ok(s) + } + t => { + debug!("unknown channel type: {:?}", t); + if let Some(ref mut enc) = self.common.encrypted { + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_OPEN_FAILURE); + enc.write.push_u32_be(sender); + enc.write.push_u32_be(3); // SSH_OPEN_UNKNOWN_CHANNEL_TYPE + enc.write.extend_ssh_string(b"Unknown channel type"); + enc.write.extend_ssh_string(b"en"); + }); + } + Ok(self) + } + } + } + fn confirm_channel_open(&mut self, channel: Channel) { + if let Some(ref mut enc) = self.common.encrypted { + server_confirm_channel_open(&mut enc.write, &channel, self.common.config.as_ref()); + enc.channels.insert(channel.sender_channel, channel); + } + } +} + +fn server_confirm_channel_open(buffer: &mut CryptoVec, channel: &Channel, config: &Config) { + push_packet!(buffer, { + buffer.push(msg::CHANNEL_OPEN_CONFIRMATION); + buffer.push_u32_be(channel.recipient_channel); // remote channel number. + buffer.push_u32_be(channel.sender_channel.0); // our channel number. + buffer.push_u32_be(config.window_size); + buffer.push_u32_be(config.maximum_packet_size); + }); +} diff --git a/thrussh/src/server/kex.rs b/thrussh/src/server/kex.rs new file mode 100644 index 00000000..a21b4812 --- /dev/null +++ b/thrussh/src/server/kex.rs @@ -0,0 +1,121 @@ +use super::*; +use crate::cipher::CipherPair; +use crate::key::PubKey; +use crate::negotiation::Select; +use crate::{kex, msg, negotiation}; +use std::cell::RefCell; +use thrussh_keys::encoding::{Encoding, Reader}; + +thread_local! { + static HASH_BUF: RefCell = RefCell::new(CryptoVec::new()); +} + +impl KexInit { + pub fn server_parse( + mut self, + config: &Config, + cipher: &CipherPair, + buf: &[u8], + write_buffer: &mut SSHBuffer, + ) -> Result { + if buf[0] == msg::KEXINIT { + let algo = { + // read algorithms from packet. + self.exchange.client_kex_init.extend(buf); + super::negotiation::Server::read_kex(buf, &config.preferred)? + }; + if !self.sent { + self.server_write(config, cipher, write_buffer)? + } + let mut key = 0; + while key < config.keys.len() && config.keys[key].name() != algo.key.as_ref() { + key += 1 + } + let next_kex = if key < config.keys.len() { + Kex::KexDh(KexDh { + exchange: self.exchange, + key: key, + names: algo, + session_id: self.session_id, + }) + } else { + return Err(Error::UnknownKey.into()); + }; + + Ok(next_kex) + } else { + Ok(Kex::KexInit(self)) + } + } + + pub fn server_write( + &mut self, + config: &Config, + cipher: &CipherPair, + write_buffer: &mut SSHBuffer, + ) -> Result<(), Error> { + self.exchange.server_kex_init.clear(); + negotiation::write_kex(&config.preferred, &mut self.exchange.server_kex_init)?; + debug!("server kex init: {:?}", &self.exchange.server_kex_init[..]); + self.sent = true; + cipher.write(&self.exchange.server_kex_init, write_buffer); + Ok(()) + } +} + +impl KexDh { + pub fn parse( + mut self, + config: &Config, + cipher: &CipherPair, + buf: &[u8], + write_buffer: &mut SSHBuffer, + ) -> Result { + if self.names.ignore_guessed { + // If we need to ignore this packet. + self.names.ignore_guessed = false; + Ok(Kex::KexDh(self)) + } else { + // Else, process it. + assert!(buf[0] == msg::KEX_ECDH_INIT); + let mut r = buf.reader(1); + self.exchange.client_ephemeral.extend(r.read_string()?); + let kex = kex::Algorithm::server_dh(self.names.kex, &mut self.exchange, buf)?; + // Then, we fill the write buffer right away, so that we + // can output it immediately when the time comes. + let kexdhdone = KexDhDone { + exchange: self.exchange, + kex: kex, + key: self.key, + names: self.names, + session_id: self.session_id, + }; + let hash: Result<_, Error> = HASH_BUF.with(|buffer| { + let mut buffer = buffer.borrow_mut(); + buffer.clear(); + debug!("server kexdhdone.exchange = {:?}", kexdhdone.exchange); + let hash = kexdhdone.kex.compute_exchange_hash( + &config.keys[kexdhdone.key], + &kexdhdone.exchange, + &mut buffer, + )?; + debug!("exchange hash: {:?}", hash); + buffer.clear(); + buffer.push(msg::KEX_ECDH_REPLY); + config.keys[kexdhdone.key].push_to(&mut buffer); + // Server ephemeral + buffer.extend_ssh_string(&kexdhdone.exchange.server_ephemeral); + // Hash signature + debug!("signing with key {:?}", kexdhdone.key); + debug!("hash: {:?}", hash); + debug!("key: {:?}", config.keys[kexdhdone.key]); + config.keys[kexdhdone.key].add_signature(&mut buffer, &hash)?; + cipher.write(&buffer, write_buffer); + cipher.write(&[msg::NEWKEYS], write_buffer); + Ok(hash) + }); + + Ok(Kex::NewKeys(kexdhdone.compute_keys(hash?, true)?)) + } + } +} diff --git a/thrussh/src/server/mod.rs b/thrussh/src/server/mod.rs new file mode 100644 index 00000000..06491027 --- /dev/null +++ b/thrussh/src/server/mod.rs @@ -0,0 +1,694 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use std; +use std::net::ToSocketAddrs; +use std::sync::Arc; + +use futures::future::Future; +use thrussh_keys::key; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; +use tokio::net::TcpListener; +use tokio::pin; + +use crate::session::*; +use crate::ssh_read::*; +use crate::sshbuffer::*; +use crate::*; + +mod kex; +mod session; +pub use self::kex::*; +pub use self::session::*; +mod encrypted; + +#[derive(Debug)] +/// Configuration of a server. +pub struct Config { + /// The server ID string sent at the beginning of the protocol. + pub server_id: String, + /// Authentication methods proposed to the client. + pub methods: auth::MethodSet, + /// The authentication banner, usually a warning message shown to the client. + pub auth_banner: Option<&'static str>, + /// Authentication rejections must happen in constant time for + /// security reasons. Thrussh does not handle this by default. + pub auth_rejection_time: std::time::Duration, + /// The server's keys. The first key pair in the client's preference order will be chosen. + pub keys: Vec, + /// The bytes and time limits before key re-exchange. + pub limits: Limits, + /// The initial size of a channel (used for flow control). + pub window_size: u32, + /// The maximal size of a single packet. + pub maximum_packet_size: u32, + /// Lists of preferred algorithms. + pub preferred: Preferred, + /// Maximal number of allowed authentication attempts. + pub max_auth_attempts: usize, + /// Time after which the connection is garbage-collected. + pub connection_timeout: Option, +} + +impl Default for Config { + fn default() -> Config { + Config { + server_id: format!( + "SSH-2.0-{}_{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + ), + methods: auth::MethodSet::all(), + auth_banner: None, + auth_rejection_time: std::time::Duration::from_secs(1), + keys: Vec::new(), + window_size: 2097152, + maximum_packet_size: 32768, + limits: Limits::default(), + preferred: Default::default(), + max_auth_attempts: 10, + connection_timeout: Some(std::time::Duration::from_secs(600)), + } + } +} + +/// A client's response in a challenge-response authentication. +#[derive(Debug)] +pub struct Response<'a> { + pos: thrussh_keys::encoding::Position<'a>, + n: u32, +} + +impl<'a> Iterator for Response<'a> { + type Item = &'a [u8]; + fn next(&mut self) -> Option { + if self.n == 0 { + None + } else { + self.n -= 1; + self.pos.read_string().ok() + } + } +} + +use std::borrow::Cow; +/// An authentication result, in a challenge-response authentication. +#[derive(Debug, PartialEq, Eq)] +pub enum Auth { + /// Reject the authentication request. + Reject, + /// Accept the authentication request. + Accept, + + /// Method was not accepted, but no other check was performed. + UnsupportedMethod, + + /// Partially accept the challenge-response authentication + /// request, providing more instructions for the client to follow. + Partial { + /// Name of this challenge. + name: Cow<'static, str>, + /// Instructions for this challenge. + instructions: Cow<'static, str>, + /// A number of prompts to the user. Each prompt has a `bool` + /// indicating whether the terminal must echo the characters + /// typed by the user. + prompts: Cow<'static, [(Cow<'static, str>, bool)]>, + }, +} + +/// Server handler. Each client will have their own handler. +pub trait Handler: Sized { + type Error: From + Send; + /// The type of authentications, which can be a future ultimately + /// resolving to + type FutureAuth: Future> + Send; + + /// The type of units returned by some parts of this handler. + type FutureUnit: Future> + Send; + + /// The type of future bools returned by some parts of this handler. + type FutureBool: Future> + Send; + + /// Convert an `Auth` to `Self::FutureAuth`. This is used to + /// produce the default handlers. + fn finished_auth(self, auth: Auth) -> Self::FutureAuth; + + /// Convert a `bool` to `Self::FutureBool`. This is used to + /// produce the default handlers. + fn finished_bool(self, b: bool, session: Session) -> Self::FutureBool; + + /// Produce a `Self::FutureUnit`. This is used to produce the + /// default handlers. + fn finished(self, session: Session) -> Self::FutureUnit; + + /// Check authentication using the "none" method. Thrussh makes + /// sure rejection happens in time `config.auth_rejection_time`, + /// except if this method takes more than that. + #[allow(unused_variables)] + fn auth_none(self, user: &str) -> Self::FutureAuth { + self.finished_auth(Auth::Reject) + } + + /// Check authentication using the "password" method. Thrussh + /// makes sure rejection happens in time + /// `config.auth_rejection_time`, except if this method takes more + /// than that. + #[allow(unused_variables)] + fn auth_password(self, user: &str, password: &str) -> Self::FutureAuth { + self.finished_auth(Auth::Reject) + } + + /// Check authentication using the "publickey" method. This method + /// should just check whether the public key matches the + /// authorized ones. Thrussh then checks the signature. If the key + /// is unknown, or the signature is invalid, Thrussh guarantees + /// that rejection happens in constant time + /// `config.auth_rejection_time`, except if this method takes more + /// time than that. + #[allow(unused_variables)] + fn auth_publickey(self, user: &str, public_key: &key::PublicKey) -> Self::FutureAuth { + self.finished_auth(Auth::Reject) + } + + /// Check authentication using the "keyboard-interactive" + /// method. Thrussh makes sure rejection happens in time + /// `config.auth_rejection_time`, except if this method takes more + /// than that. + #[allow(unused_variables)] + fn auth_keyboard_interactive( + self, + user: &str, + submethods: &str, + response: Option, + ) -> Self::FutureAuth { + self.finished_auth(Auth::Reject) + } + + /// Called when the client closes a channel. + #[allow(unused_variables)] + fn channel_close(self, channel: ChannelId, session: Session) -> Self::FutureUnit { + self.finished(session) + } + + /// Called when the client sends EOF to a channel. + #[allow(unused_variables)] + fn channel_eof(self, channel: ChannelId, session: Session) -> Self::FutureUnit { + self.finished(session) + } + + /// Called when a new session channel is created. + #[allow(unused_variables)] + fn channel_open_session(self, channel: ChannelId, session: Session) -> Self::FutureUnit { + self.finished(session) + } + + /// Called when a new X11 channel is created. + #[allow(unused_variables)] + fn channel_open_x11( + self, + channel: ChannelId, + originator_address: &str, + originator_port: u32, + session: Session, + ) -> Self::FutureUnit { + self.finished(session) + } + + /// Called when a new channel is created. + #[allow(unused_variables)] + fn channel_open_direct_tcpip( + self, + channel: ChannelId, + host_to_connect: &str, + port_to_connect: u32, + originator_address: &str, + originator_port: u32, + session: Session, + ) -> Self::FutureUnit { + self.finished(session) + } + + /// Called when a data packet is received. A response can be + /// written to the `response` argument. + #[allow(unused_variables)] + fn data(self, channel: ChannelId, data: &[u8], session: Session) -> Self::FutureUnit { + self.finished(session) + } + + /// Called when an extended data packet is received. Code 1 means + /// that this packet comes from stderr, other codes are not + /// defined (see + /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-5.2)). + #[allow(unused_variables)] + fn extended_data( + self, + channel: ChannelId, + code: u32, + data: &[u8], + session: Session, + ) -> Self::FutureUnit { + self.finished(session) + } + + /// Called when the network window is adjusted, meaning that we + /// can send more bytes. + #[allow(unused_variables)] + fn window_adjusted( + self, + channel: ChannelId, + new_window_size: usize, + mut session: Session, + ) -> Self::FutureUnit { + if let Some(ref mut enc) = session.common.encrypted { + enc.flush_pending(channel); + } + self.finished(session) + } + + /// Called when this server adjusts the network window. Return the + /// next target window. + #[allow(unused_variables)] + fn adjust_window(&mut self, channel: ChannelId, current: u32) -> u32 { + current + } + + /// The client requests a pseudo-terminal with the given + /// specifications. + #[allow(unused_variables)] + fn pty_request( + self, + channel: ChannelId, + term: &str, + col_width: u32, + row_height: u32, + pix_width: u32, + pix_height: u32, + modes: &[(Pty, u32)], + session: Session, + ) -> Self::FutureUnit { + self.finished(session) + } + + /// The client requests an X11 connection. + #[allow(unused_variables)] + fn x11_request( + self, + channel: ChannelId, + single_connection: bool, + x11_auth_protocol: &str, + x11_auth_cookie: &str, + x11_screen_number: u32, + session: Session, + ) -> Self::FutureUnit { + self.finished(session) + } + + /// The client wants to set the given environment variable. Check + /// these carefully, as it is dangerous to allow any variable + /// environment to be set. + #[allow(unused_variables)] + fn env_request( + self, + channel: ChannelId, + variable_name: &str, + variable_value: &str, + session: Session, + ) -> Self::FutureUnit { + self.finished(session) + } + + /// The client requests a shell. + #[allow(unused_variables)] + fn shell_request(self, channel: ChannelId, session: Session) -> Self::FutureUnit { + self.finished(session) + } + + /// The client sends a command to execute, to be passed to a + /// shell. Make sure to check the command before doing so. + #[allow(unused_variables)] + fn exec_request(self, channel: ChannelId, data: &[u8], session: Session) -> Self::FutureUnit { + self.finished(session) + } + + /// The client asks to start the subsystem with the given name + /// (such as sftp). + #[allow(unused_variables)] + fn subsystem_request( + self, + channel: ChannelId, + name: &str, + session: Session, + ) -> Self::FutureUnit { + self.finished(session) + } + + /// The client's pseudo-terminal window size has changed. + #[allow(unused_variables)] + fn window_change_request( + self, + channel: ChannelId, + col_width: u32, + row_height: u32, + pix_width: u32, + pix_height: u32, + session: Session, + ) -> Self::FutureUnit { + self.finished(session) + } + + /// The client is sending a signal (usually to pass to the + /// currently running process). + #[allow(unused_variables)] + fn signal(self, channel: ChannelId, signal_name: Sig, session: Session) -> Self::FutureUnit { + self.finished(session) + } + + /// Used for reverse-forwarding ports, see + /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-7). + #[allow(unused_variables)] + fn tcpip_forward(self, address: &str, port: u32, session: Session) -> Self::FutureBool { + self.finished_bool(false, session) + } + /// Used to stop the reverse-forwarding of a port, see + /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-7). + #[allow(unused_variables)] + fn cancel_tcpip_forward(self, address: &str, port: u32, session: Session) -> Self::FutureBool { + self.finished_bool(false, session) + } +} + +/// Trait used to create new handlers when clients connect. +pub trait Server { + /// The type of handlers. + type Handler: Handler + Send; + /// Called when a new client connects. + fn new(&mut self, peer_addr: Option) -> Self::Handler; +} + +/// Run a server. +/// Create a new `Connection` from the server's configuration, a +/// stream and a [`Handler`](trait.Handler.html). +pub async fn run( + config: Arc, + addr: &str, + mut server: H, +) -> Result<(), std::io::Error> { + let addr = addr.to_socket_addrs().unwrap().next().unwrap(); + let socket = TcpListener::bind(&addr).await?; + if config.maximum_packet_size > 65535 { + error!( + "Maximum packet size ({:?}) should not larger than a TCP packet (65535)", + config.maximum_packet_size + ); + } + while let Ok((socket, _)) = socket.accept().await { + let config = config.clone(); + let server = server.new(socket.peer_addr().ok()); + tokio::spawn(run_stream(config, socket, server)); + } + Ok(()) +} + +use std::cell::RefCell; +thread_local! { + static B1: RefCell = RefCell::new(CryptoVec::new()); + static B2: RefCell = RefCell::new(CryptoVec::new()); +} + +pub async fn timeout(delay: Option) { + if let Some(delay) = delay { + tokio::time::sleep(delay).await + } else { + futures::future::pending().await + }; +} + +async fn start_reading( + mut stream_read: R, + mut buffer: SSHBuffer, + cipher: Arc, +) -> Result<(usize, R, SSHBuffer), Error> { + buffer.buffer.clear(); + let n = cipher::read(&mut stream_read, &mut buffer, &cipher).await?; + Ok((n, stream_read, buffer)) +} + +pub async fn run_stream( + config: Arc, + mut stream: R, + handler: H, +) -> Result +where + R: AsyncRead + AsyncWrite + Unpin, +{ + let mut handler = Some(handler); + let delay = config.connection_timeout; + // Writing SSH id. + let mut decomp = CryptoVec::new(); + let mut write_buffer = SSHBuffer::new(); + write_buffer.send_ssh_id(config.as_ref().server_id.as_bytes()); + stream + .write_all(&write_buffer.buffer[..]) + .await + .map_err(crate::Error::from)?; + + // Reading SSH id and allocating a session. + let mut stream = SshRead::new(&mut stream); + let common = read_ssh_id(config, &mut stream).await?; + let (sender, receiver) = tokio::sync::mpsc::channel(10); + let mut session = Session { + target_window_size: common.config.window_size, + common, + receiver, + sender: server::session::Handle { sender }, + pending_reads: Vec::new(), + pending_len: 0, + }; + session.flush()?; + stream + .write_all(&session.common.write_buffer.buffer) + .await + .map_err(crate::Error::from)?; + session.common.write_buffer.buffer.clear(); + + let (stream_read, mut stream_write) = stream.split(); + let buffer = SSHBuffer::new(); + let reading = start_reading(stream_read, buffer, session.common.cipher.clone()); + pin!(reading); + let mut is_reading = None; + + while !session.common.disconnected { + tokio::select! { + r = &mut reading => { + let (stream_read, buffer) = match r { + Ok((_, stream_read, buffer)) => (stream_read, buffer), + Err(e) => return Err(e.into()) + }; + if buffer.buffer.len() < 5 { + is_reading = Some((stream_read, buffer)); + break + } + let buf = if let Some(ref mut enc) = session.common.encrypted { + let d = enc.decompress.decompress( + &buffer.buffer[5..], + &mut decomp, + ); + if let Ok(buf) = d { + buf + } else { + debug!("err = {:?}", d); + is_reading = Some((stream_read, buffer)); + break + } + } else { + &buffer.buffer[5..] + }; + if !buf.is_empty() { + if buf[0] == crate::msg::DISCONNECT { + debug!("break"); + is_reading = Some((stream_read, buffer)); + break; + } else if buf[0] > 4 { + match reply(session, &mut handler, &buf[..]).await { + Ok(s) => session = s, + Err(e) => return Err(e), + } + } + } + reading.set(start_reading(stream_read, buffer, session.common.cipher.clone())); + } + _ = timeout(delay) => { + debug!("timeout"); + break + }, + msg = session.receiver.recv(), if !session.is_rekeying() => { + match msg { + Some((id, ChannelMsg::Data { data })) => { + session.data(id, data); + } + Some((id, ChannelMsg::ExtendedData { ext, data })) => { + session.extended_data(id, ext, data); + } + Some((id, ChannelMsg::Eof)) => { + session.eof(id); + } + Some((id, ChannelMsg::Close)) => { + session.close(id); + } + Some((id, ChannelMsg::XonXoff { client_can_do })) => { + session.xon_xoff_request(id, client_can_do); + } + Some((id, ChannelMsg::ExitStatus { exit_status })) => { + session.exit_status_request(id, exit_status); + } + Some((id, ChannelMsg::ExitSignal { signal_name, core_dumped, error_message, lang_tag })) => { + session.exit_signal_request(id, signal_name, core_dumped, &error_message, &lang_tag); + } + Some((id, ChannelMsg::WindowAdjusted { new_size })) => { + debug!("window adjusted to {:?} for channel {:?}", new_size, id); + } + Some((id, ChannelMsg::Success)) => { + debug!("channel success {:?}", id); + } + None => { + debug!("session.receiver: received None"); + } + } + } + } + session.flush()?; + stream_write + .write_all(&session.common.write_buffer.buffer) + .await + .map_err(crate::Error::from)?; + session.common.write_buffer.buffer.clear(); + } + debug!("disconnected"); + // Shutdown + stream_write.shutdown().await.map_err(crate::Error::from)?; + loop { + if let Some((stream_read, buffer)) = is_reading.take() { + reading.set(start_reading( + stream_read, + buffer, + session.common.cipher.clone(), + )); + } + let (n, r, b) = (&mut reading).await?; + is_reading = Some((r, b)); + if n == 0 { + break; + } + } + Ok(handler.unwrap()) +} + +async fn read_ssh_id( + config: Arc, + read: &mut SshRead, +) -> Result>, Error> { + let sshid = if let Some(t) = config.connection_timeout { + tokio::time::timeout(t, read.read_ssh_id()).await?? + } else { + read.read_ssh_id().await? + }; + let mut exchange = Exchange::new(); + exchange.client_id.extend(sshid); + // Preparing the response + exchange + .server_id + .extend(config.as_ref().server_id.as_bytes()); + let mut kexinit = KexInit { + exchange: exchange, + algo: None, + sent: false, + session_id: None, + }; + let cipher = Arc::new(cipher::CLEAR_PAIR); + let mut write_buffer = SSHBuffer::new(); + kexinit.server_write(config.as_ref(), cipher.as_ref(), &mut write_buffer)?; + Ok(CommonSession { + write_buffer, + kex: Some(Kex::KexInit(kexinit)), + auth_user: String::new(), + auth_method: None, // Client only. + cipher, + encrypted: None, + config: config, + wants_reply: false, + disconnected: false, + buffer: CryptoVec::new(), + }) +} + +async fn reply( + mut session: Session, + handler: &mut Option, + buf: &[u8], +) -> Result { + // Handle key exchange/re-exchange. + if session.common.encrypted.is_none() { + match session.common.kex.take() { + Some(Kex::KexInit(kexinit)) => { + if kexinit.algo.is_some() || buf[0] == msg::KEXINIT { + session.common.kex = Some(kexinit.server_parse( + session.common.config.as_ref(), + &session.common.cipher, + &buf, + &mut session.common.write_buffer, + )?); + return Ok(session); + } else { + // Else, i.e. if the other side has not started + // the key exchange, process its packets by simple + // not returning. + session.common.kex = Some(Kex::KexInit(kexinit)) + } + } + Some(Kex::KexDh(kexdh)) => { + session.common.kex = Some(kexdh.parse( + session.common.config.as_ref(), + &session.common.cipher, + buf, + &mut session.common.write_buffer, + )?); + return Ok(session); + } + Some(Kex::NewKeys(newkeys)) => { + if buf[0] != msg::NEWKEYS { + return Err(Error::Kex.into()); + } + // Ok, NEWKEYS received, now encrypted. + session.common.encrypted( + EncryptedState::WaitingServiceRequest { + sent: false, + accepted: false, + }, + newkeys, + ); + return Ok(session); + } + Some(kex) => { + session.common.kex = Some(kex); + return Ok(session); + } + None => {} + } + Ok(session) + } else { + Ok(session.server_read_encrypted(handler, buf).await?) + } +} diff --git a/thrussh/src/server/session.rs b/thrussh/src/server/session.rs new file mode 100644 index 00000000..f66906b5 --- /dev/null +++ b/thrussh/src/server/session.rs @@ -0,0 +1,438 @@ +use super::*; +use crate::msg; +use std::sync::Arc; +use thrussh_keys::encoding::Encoding; +use tokio::sync::mpsc::{Receiver, Sender}; + +/// A connected server session. This type is unique to a client. +pub struct Session { + pub(crate) common: CommonSession>, + pub(crate) sender: Handle, + pub(crate) receiver: Receiver<(ChannelId, ChannelMsg)>, + pub(crate) target_window_size: u32, + pub(crate) pending_reads: Vec, + pub(crate) pending_len: u32, +} + +#[derive(Clone)] +/// Handle to a session, used to send messages to a client outside of +/// the request/response cycle. +pub struct Handle { + pub(crate) sender: Sender<(ChannelId, ChannelMsg)>, +} + +impl Handle { + /// Send data to the session referenced by this handler. + pub async fn data(&mut self, id: ChannelId, data: CryptoVec) -> Result<(), CryptoVec> { + self.sender + .send((id, ChannelMsg::Data { data })) + .await + .map_err(|e| match e.0 { + (_, ChannelMsg::Data { data }) => data, + _ => unreachable!(), + }) + } + + /// Send data to the session referenced by this handler. + pub async fn extended_data( + &mut self, + id: ChannelId, + ext: u32, + data: CryptoVec, + ) -> Result<(), CryptoVec> { + self.sender + .send((id, ChannelMsg::ExtendedData { ext, data })) + .await + .map_err(|e| match e.0 { + (_, ChannelMsg::ExtendedData { data, .. }) => data, + _ => unreachable!(), + }) + } + + /// Send EOF to the session referenced by this handler. + pub async fn eof(&mut self, id: ChannelId) -> Result<(), ()> { + self.sender + .send((id, ChannelMsg::Eof)) + .await + .map_err(|_| ()) + } + + /// Close a channel. + pub async fn close(&mut self, id: ChannelId) -> Result<(), ()> { + self.sender + .send((id, ChannelMsg::Close)) + .await + .map_err(|_| ()) + } + + /// Inform the client of whether they may perform + /// control-S/control-Q flow control. See + /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-6.8). + pub async fn xon_xoff_request(&mut self, id: ChannelId, client_can_do: bool) -> Result<(), ()> { + self.sender + .send((id, ChannelMsg::XonXoff { client_can_do })) + .await + .map_err(|_| ()) + } + + /// Send the exit status of a program. + pub async fn exit_status_request(&mut self, id: ChannelId, exit_status: u32) -> Result<(), ()> { + self.sender + .send((id, ChannelMsg::ExitStatus { exit_status })) + .await + .map_err(|_| ()) + } + + /// If the program was killed by a signal, send the details about the signal to the client. + pub async fn exit_signal_request( + &mut self, + id: ChannelId, + signal_name: Sig, + core_dumped: bool, + error_message: String, + lang_tag: String, + ) -> Result<(), ()> { + self.sender + .send(( + id, + ChannelMsg::ExitSignal { + signal_name, + core_dumped, + error_message, + lang_tag, + }, + )) + .await + .map_err(|_| ()) + } +} + +impl Session { + pub(crate) fn is_rekeying(&self) -> bool { + if let Some(ref enc) = self.common.encrypted { + enc.rekey.is_some() + } else { + true + } + } + + /// Get a handle to this session. + pub fn handle(&self) -> Handle { + self.sender.clone() + } + + pub fn writable_packet_size(&self, channel: &ChannelId) -> u32 { + if let Some(ref enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + return channel + .sender_window_size + .min(channel.sender_maximum_packet_size); + } + } + 0 + } + + pub fn window_size(&self, channel: &ChannelId) -> u32 { + if let Some(ref enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + return channel.sender_window_size; + } + } + 0 + } + + pub fn max_packet_size(&self, channel: &ChannelId) -> u32 { + if let Some(ref enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + return channel.sender_maximum_packet_size; + } + } + 0 + } + + /// Flush the session, i.e. encrypt the pending buffer. + pub fn flush(&mut self) -> Result<(), Error> { + if let Some(ref mut enc) = self.common.encrypted { + if enc.flush( + &self.common.config.as_ref().limits, + &self.common.cipher, + &mut self.common.write_buffer, + ) { + if enc.rekey.is_none() { + debug!("starting rekeying"); + if let Some(exchange) = enc.exchange.take() { + let mut kexinit = KexInit::initiate_rekey(exchange, &enc.session_id); + kexinit.server_write( + &self.common.config.as_ref(), + &mut self.common.cipher, + &mut self.common.write_buffer, + )?; + enc.rekey = Some(Kex::KexInit(kexinit)) + } + } + } + } + Ok(()) + } + + pub fn flush_pending(&mut self, channel: ChannelId) -> usize { + if let Some(ref mut enc) = self.common.encrypted { + enc.flush_pending(channel) + } else { + 0 + } + } + + pub fn sender_window_size(&self, channel: ChannelId) -> usize { + if let Some(ref enc) = self.common.encrypted { + enc.sender_window_size(channel) + } else { + 0 + } + } + + pub fn has_pending_data(&self, channel: ChannelId) -> bool { + if let Some(ref enc) = self.common.encrypted { + enc.has_pending_data(channel) + } else { + false + } + } + + /// Retrieves the configuration of this session. + pub fn config(&self) -> &Config { + &self.common.config + } + + /// Sends a disconnect message. + pub fn disconnect(&mut self, reason: Disconnect, description: &str, language_tag: &str) { + self.common.disconnect(reason, description, language_tag); + } + + /// Send a "success" reply to a /global/ request (requests without + /// a channel number, such as TCP/IP forwarding or + /// cancelling). Always call this function if the request was + /// successful (it checks whether the client expects an answer). + pub fn request_success(&mut self) { + if self.common.wants_reply { + if let Some(ref mut enc) = self.common.encrypted { + self.common.wants_reply = false; + push_packet!(enc.write, enc.write.push(msg::REQUEST_SUCCESS)) + } + } + } + + /// Send a "failure" reply to a global request. + pub fn request_failure(&mut self) { + if let Some(ref mut enc) = self.common.encrypted { + self.common.wants_reply = false; + push_packet!(enc.write, enc.write.push(msg::REQUEST_FAILURE)) + } + } + + /// Send a "success" reply to a channel request. Always call this + /// function if the request was successful (it checks whether the + /// client expects an answer). + pub fn channel_success(&mut self, channel: ChannelId) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get_mut(&channel) { + assert!(channel.confirmed); + if channel.wants_reply { + channel.wants_reply = false; + debug!("channel_success {:?}", channel); + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_SUCCESS); + enc.write.push_u32_be(channel.recipient_channel); + }) + } + } + } + } + + /// Send a "failure" reply to a global request. + pub fn channel_failure(&mut self, channel: ChannelId) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get_mut(&channel) { + assert!(channel.confirmed); + if channel.wants_reply { + channel.wants_reply = false; + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_FAILURE); + enc.write.push_u32_be(channel.recipient_channel); + }) + } + } + } + } + + /// Send a "failure" reply to a request to open a channel open. + pub fn channel_open_failure( + &mut self, + channel: ChannelId, + reason: ChannelOpenFailure, + description: &str, + language: &str, + ) { + if let Some(ref mut enc) = self.common.encrypted { + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_OPEN_FAILURE); + enc.write.push_u32_be(channel.0); + enc.write.push_u32_be(reason as u32); + enc.write.extend_ssh_string(description.as_bytes()); + enc.write.extend_ssh_string(language.as_bytes()); + }) + } + } + + /// Close a channel. + pub fn close(&mut self, channel: ChannelId) { + self.common.byte(channel, msg::CHANNEL_CLOSE); + } + + /// Send EOF to a channel + pub fn eof(&mut self, channel: ChannelId) { + self.common.byte(channel, msg::CHANNEL_EOF); + } + + /// Send data to a channel. On session channels, `extended` can be + /// used to encode standard error by passing `Some(1)`, and stdout + /// by passing `None`. + /// + /// The number of bytes added to the "sending pipeline" (to be + /// processed by the event loop) is returned. + pub fn data(&mut self, channel: ChannelId, data: CryptoVec) { + if let Some(ref mut enc) = self.common.encrypted { + enc.data(channel, data) + } else { + unreachable!() + } + } + + /// Send data to a channel. On session channels, `extended` can be + /// used to encode standard error by passing `Some(1)`, and stdout + /// by passing `None`. + /// + /// The number of bytes added to the "sending pipeline" (to be + /// processed by the event loop) is returned. + pub fn extended_data(&mut self, channel: ChannelId, extended: u32, data: CryptoVec) { + if let Some(ref mut enc) = self.common.encrypted { + enc.extended_data(channel, extended, data) + } else { + unreachable!() + } + } + + /// Inform the client of whether they may perform + /// control-S/control-Q flow control. See + /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-6.8). + pub fn xon_xoff_request(&mut self, channel: ChannelId, client_can_do: bool) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + assert!(channel.confirmed); + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_REQUEST); + + enc.write.push_u32_be(channel.recipient_channel); + enc.write.extend_ssh_string(b"xon-xoff"); + enc.write.push(0); + enc.write.push(if client_can_do { 1 } else { 0 }); + }) + } + } + } + + /// Send the exit status of a program. + pub fn exit_status_request(&mut self, channel: ChannelId, exit_status: u32) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + assert!(channel.confirmed); + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_REQUEST); + + enc.write.push_u32_be(channel.recipient_channel); + enc.write.extend_ssh_string(b"exit-status"); + enc.write.push(0); + enc.write.push_u32_be(exit_status) + }) + } + } + } + + /// If the program was killed by a signal, send the details about the signal to the client. + pub fn exit_signal_request( + &mut self, + channel: ChannelId, + signal: Sig, + core_dumped: bool, + error_message: &str, + language_tag: &str, + ) { + if let Some(ref mut enc) = self.common.encrypted { + if let Some(channel) = enc.channels.get(&channel) { + assert!(channel.confirmed); + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_REQUEST); + + enc.write.push_u32_be(channel.recipient_channel); + enc.write.extend_ssh_string(b"exit-signal"); + enc.write.push(0); + enc.write.extend_ssh_string(signal.name().as_bytes()); + enc.write.push(if core_dumped { 1 } else { 0 }); + enc.write.extend_ssh_string(error_message.as_bytes()); + enc.write.extend_ssh_string(language_tag.as_bytes()); + }) + } + } + } + + /// Open a TCP/IP forwarding channel, when a connection comes to a + /// local port for which forwarding has been requested. See + /// [RFC4254](https://tools.ietf.org/html/rfc4254#section-7). The + /// TCP/IP packets can then be tunneled through the channel using + /// `.data()`. + pub fn channel_open_forwarded_tcpip( + &mut self, + connected_address: &str, + connected_port: u32, + originator_address: &str, + originator_port: u32, + ) -> Result { + let result = if let Some(ref mut enc) = self.common.encrypted { + match enc.state { + EncryptedState::Authenticated => { + debug!("sending open request"); + + let sender_channel = enc.new_channel( + self.common.config.window_size, + self.common.config.maximum_packet_size, + ); + push_packet!(enc.write, { + enc.write.push(msg::CHANNEL_OPEN); + enc.write.extend_ssh_string(b"forwarded-tcpip"); + + // sender channel id. + enc.write.push_u32_be(sender_channel.0); + + // window. + enc.write + .push_u32_be(self.common.config.as_ref().window_size); + + // max packet size. + enc.write + .push_u32_be(self.common.config.as_ref().maximum_packet_size); + + enc.write.extend_ssh_string(connected_address.as_bytes()); + enc.write.push_u32_be(connected_port); // sender channel id. + enc.write.extend_ssh_string(originator_address.as_bytes()); + enc.write.push_u32_be(originator_port); // sender channel id. + }); + sender_channel + } + _ => return Err(Error::Inconsistent), + } + } else { + return Err(Error::Inconsistent); + }; + Ok(result) + } +} diff --git a/thrussh/src/session.rs b/thrussh/src/session.rs new file mode 100644 index 00000000..f0483d26 --- /dev/null +++ b/thrussh/src/session.rs @@ -0,0 +1,510 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use crate::sshbuffer::SSHBuffer; +use crate::{auth, cipher, kex, msg, negotiation}; +use crate::{Channel, ChannelId, Disconnect, Limits}; +use byteorder::{BigEndian, ByteOrder}; +use cryptovec::CryptoVec; +use std::collections::HashMap; +use std::num::Wrapping; +use std::sync::Arc; +use thrussh_keys::encoding::Encoding; + +#[derive(Debug)] +pub(crate) struct Encrypted { + pub state: EncryptedState, + + // It's always Some, except when we std::mem::replace it temporarily. + pub exchange: Option, + pub kex: kex::Algorithm, + pub key: usize, + pub mac: Option<&'static str>, + pub session_id: crate::Sha256Hash, + pub rekey: Option, + pub channels: HashMap, + pub last_channel_id: Wrapping, + pub wants_reply: bool, + pub write: CryptoVec, + pub write_cursor: usize, + pub last_rekey: std::time::Instant, + pub server_compression: crate::compression::Compression, + pub client_compression: crate::compression::Compression, + pub compress: crate::compression::Compress, + pub decompress: crate::compression::Decompress, + pub compress_buffer: CryptoVec, +} + +pub(crate) struct CommonSession { + pub auth_user: String, + pub config: Config, + pub encrypted: Option, + pub auth_method: Option, + pub write_buffer: SSHBuffer, + pub kex: Option, + pub cipher: Arc, + pub wants_reply: bool, + pub disconnected: bool, + pub buffer: CryptoVec, +} + +impl CommonSession { + pub fn newkeys(&mut self, newkeys: NewKeys) { + if let Some(ref mut enc) = self.encrypted { + enc.exchange = Some(newkeys.exchange); + enc.kex = newkeys.kex; + enc.key = newkeys.key; + enc.mac = newkeys.names.mac; + self.cipher = Arc::new(newkeys.cipher); + } + } + + pub fn encrypted(&mut self, state: EncryptedState, newkeys: NewKeys) { + self.encrypted = Some(Encrypted { + exchange: Some(newkeys.exchange), + kex: newkeys.kex, + key: newkeys.key, + mac: newkeys.names.mac, + session_id: newkeys.session_id, + state, + rekey: None, + channels: HashMap::new(), + last_channel_id: Wrapping(1), + wants_reply: false, + write: CryptoVec::new(), + write_cursor: 0, + last_rekey: std::time::Instant::now(), + server_compression: newkeys.names.server_compression, + client_compression: newkeys.names.client_compression, + compress: crate::compression::Compress::None, + compress_buffer: CryptoVec::new(), + decompress: crate::compression::Decompress::None, + }); + self.cipher = Arc::new(newkeys.cipher); + } + + /// Send a disconnect message. + pub fn disconnect(&mut self, reason: Disconnect, description: &str, language_tag: &str) { + let disconnect = |buf: &mut CryptoVec| { + push_packet!(buf, { + buf.push(msg::DISCONNECT); + buf.push_u32_be(reason as u32); + buf.extend_ssh_string(description.as_bytes()); + buf.extend_ssh_string(language_tag.as_bytes()); + }); + }; + if !self.disconnected { + self.disconnected = true; + if let Some(ref mut enc) = self.encrypted { + disconnect(&mut enc.write) + } else { + disconnect(&mut self.write_buffer.buffer) + } + } + } + + /// Send a single byte message onto the channel. + pub fn byte(&mut self, channel: ChannelId, msg: u8) { + if let Some(ref mut enc) = self.encrypted { + enc.byte(channel, msg) + } + } +} + +impl Encrypted { + pub fn byte(&mut self, channel: ChannelId, msg: u8) { + if let Some(channel) = self.channels.get(&channel) { + push_packet!(self.write, { + self.write.push(msg); + self.write.push_u32_be(channel.recipient_channel); + }); + } + } + + /* + pub fn authenticated(&mut self) { + self.server_compression.init_compress(&mut self.compress); + self.state = EncryptedState::Authenticated; + } + */ + + pub fn eof(&mut self, channel: ChannelId) { + self.byte(channel, msg::CHANNEL_EOF); + } + + pub fn sender_window_size(&self, channel: ChannelId) -> usize { + if let Some(ref channel) = self.channels.get(&channel) { + channel.sender_window_size as usize + } else { + 0 + } + } + + pub fn adjust_window_size(&mut self, channel: ChannelId, data: &[u8], target: u32) -> bool { + debug!("adjust_window_size"); + if let Some(ref mut channel) = self.channels.get_mut(&channel) { + debug!("channel {:?}", channel); + // Ignore extra data. + // https://tools.ietf.org/html/rfc4254#section-5.2 + if data.len() as u32 <= channel.sender_window_size { + channel.sender_window_size -= data.len() as u32; + } + if channel.sender_window_size < target / 2 { + debug!( + "sender_window_size {:?}, target {:?}", + channel.sender_window_size, target + ); + push_packet!(self.write, { + self.write.push(msg::CHANNEL_WINDOW_ADJUST); + self.write.push_u32_be(channel.recipient_channel); + self.write.push_u32_be(target - channel.sender_window_size); + }); + channel.sender_window_size = target; + return true; + } + } + false + } + + pub fn flush_pending(&mut self, channel: ChannelId) -> usize { + let mut pending_size = 0; + if let Some(channel) = self.channels.get_mut(&channel) { + while let Some((buf, a, from)) = channel.pending_data.pop_front() { + let size = Self::data_noqueue(&mut self.write, channel, &buf, from); + pending_size += size; + if from + size < buf.len() { + channel.pending_data.push_front((buf, a, from + size)); + break; + } + } + } + pending_size + } + + pub fn flush_all_pending(&mut self) { + for (_, channel) in self.channels.iter_mut() { + while let Some((buf, a, from)) = channel.pending_data.pop_front() { + let size = Self::data_noqueue(&mut self.write, channel, &buf, from); + if from + size < buf.len() { + channel.pending_data.push_front((buf, a, from + size)); + break; + } + } + } + } + + pub fn has_pending_data(&self, channel: ChannelId) -> bool { + if let Some(channel) = self.channels.get(&channel) { + !channel.pending_data.is_empty() + } else { + false + } + } + + /// Push the largest amount of `&buf0[from..]` that can fit into + /// the window, dividing it into packets if it is too large, and + /// return the length that was written. + fn data_noqueue( + write: &mut CryptoVec, + channel: &mut Channel, + buf0: &[u8], + from: usize, + ) -> usize { + let mut buf = if buf0.len() as u32 > from as u32 + channel.recipient_window_size { + &buf0[from..from + channel.recipient_window_size as usize] + } else { + &buf0[from..] + }; + let buf_len = buf.len(); + + while buf.len() > 0 { + // Compute the length we're allowed to send. + let off = std::cmp::min(buf.len(), channel.recipient_maximum_packet_size as usize); + push_packet!(write, { + write.push(msg::CHANNEL_DATA); + write.push_u32_be(channel.recipient_channel); + write.extend_ssh_string(&buf[..off]); + }); + debug!( + "buffer: {:?} {:?}", + write.len(), + channel.recipient_window_size + ); + channel.recipient_window_size -= off as u32; + buf = &buf[off..] + } + debug!("buf.len() = {:?}, buf_len = {:?}", buf.len(), buf_len); + buf_len + } + + pub fn data(&mut self, channel: ChannelId, buf0: CryptoVec) { + if let Some(channel) = self.channels.get_mut(&channel) { + assert!(channel.confirmed); + if !channel.pending_data.is_empty() || self.rekey.is_some() { + channel.pending_data.push_back((buf0, None, 0)); + return; + } + let buf_len = Self::data_noqueue(&mut self.write, channel, &buf0, 0); + if buf_len < buf0.len() { + channel.pending_data.push_back((buf0, None, buf_len)) + } + } + } + + pub fn extended_data(&mut self, channel: ChannelId, ext: u32, buf0: CryptoVec) { + use std::ops::Deref; + if let Some(channel) = self.channels.get_mut(&channel) { + assert!(channel.confirmed); + if !channel.pending_data.is_empty() { + channel.pending_data.push_back((buf0, Some(ext), 0)); + return; + } + let mut buf = if buf0.len() as u32 > channel.recipient_window_size { + &buf0[0..channel.recipient_window_size as usize] + } else { + &buf0 + }; + let buf_len = buf.len(); + + while buf.len() > 0 { + // Compute the length we're allowed to send. + let off = std::cmp::min(buf.len(), channel.recipient_maximum_packet_size as usize); + push_packet!(self.write, { + self.write.push(msg::CHANNEL_EXTENDED_DATA); + self.write.push_u32_be(channel.recipient_channel); + self.write.push_u32_be(ext); + self.write.extend_ssh_string(&buf[..off]); + }); + debug!("buffer: {:?}", self.write.deref().len()); + channel.recipient_window_size -= off as u32; + buf = &buf[off..] + } + debug!("buf.len() = {:?}, buf_len = {:?}", buf.len(), buf_len); + if buf_len < buf0.len() { + channel.pending_data.push_back((buf0, Some(ext), buf_len)) + } + } + } + + pub fn flush( + &mut self, + limits: &Limits, + cipher: &cipher::CipherPair, + write_buffer: &mut SSHBuffer, + ) -> bool { + // If there are pending packets (and we've not started to rekey), flush them. + { + while self.write_cursor < self.write.len() { + // Read a single packet, encrypt and send it. + let len = BigEndian::read_u32(&self.write[self.write_cursor..]) as usize; + let packet = self + .compress + .compress( + &self.write[(self.write_cursor + 4)..(self.write_cursor + 4 + len)], + &mut self.compress_buffer, + ) + .unwrap(); + cipher.write(packet, write_buffer); + self.write_cursor += 4 + len + } + } + if self.write_cursor >= self.write.len() { + // If all packets have been written, clear. + self.write_cursor = 0; + self.write.clear(); + } + let now = std::time::Instant::now(); + let dur = now.duration_since(self.last_rekey); + write_buffer.bytes >= limits.rekey_write_limit || dur >= limits.rekey_time_limit + } + pub fn new_channel_id(&mut self) -> ChannelId { + self.last_channel_id += Wrapping(1); + while self + .channels + .contains_key(&ChannelId(self.last_channel_id.0)) + { + self.last_channel_id += Wrapping(1) + } + ChannelId(self.last_channel_id.0) + } + pub fn new_channel(&mut self, window_size: u32, maxpacket: u32) -> ChannelId { + loop { + self.last_channel_id += Wrapping(1); + if let std::collections::hash_map::Entry::Vacant(vacant_entry) = + self.channels.entry(ChannelId(self.last_channel_id.0)) + { + vacant_entry.insert(Channel { + recipient_channel: 0, + sender_channel: ChannelId(self.last_channel_id.0), + sender_window_size: window_size, + recipient_window_size: 0, + sender_maximum_packet_size: maxpacket, + recipient_maximum_packet_size: 0, + confirmed: false, + wants_reply: false, + pending_data: std::collections::VecDeque::new(), + }); + return ChannelId(self.last_channel_id.0); + } + } + } +} + +#[derive(Debug)] +pub enum EncryptedState { + WaitingServiceRequest { sent: bool, accepted: bool }, + WaitingAuthRequest(auth::AuthRequest), + InitCompression, + Authenticated, +} + +#[derive(Debug)] +pub struct Exchange { + pub client_id: CryptoVec, + pub server_id: CryptoVec, + pub client_kex_init: CryptoVec, + pub server_kex_init: CryptoVec, + pub client_ephemeral: CryptoVec, + pub server_ephemeral: CryptoVec, +} + +impl Exchange { + pub fn new() -> Self { + Exchange { + client_id: CryptoVec::new(), + server_id: CryptoVec::new(), + client_kex_init: CryptoVec::new(), + server_kex_init: CryptoVec::new(), + client_ephemeral: CryptoVec::new(), + server_ephemeral: CryptoVec::new(), + } + } +} + +#[derive(Debug)] +pub enum Kex { + /// Version number sent. `algo` and `sent` tell wether kexinit has + /// been received, and sent, respectively. + KexInit(KexInit), + + /// Algorithms have been determined, the DH algorithm should run. + KexDh(KexDh), + + /// The kex has run. + KexDhDone(KexDhDone), + + /// The DH is over, we've sent the NEWKEYS packet, and are waiting + /// the NEWKEYS from the other side. + NewKeys(NewKeys), +} + +#[derive(Debug)] +pub struct KexInit { + pub algo: Option, + pub exchange: Exchange, + pub session_id: Option, + pub sent: bool, +} + +impl KexInit { + pub fn received_rekey( + ex: Exchange, + algo: negotiation::Names, + session_id: &crate::Sha256Hash, + ) -> Self { + let mut kexinit = KexInit { + exchange: ex, + algo: Some(algo), + sent: false, + session_id: Some(session_id.clone()), + }; + kexinit.exchange.client_kex_init.clear(); + kexinit.exchange.server_kex_init.clear(); + kexinit.exchange.client_ephemeral.clear(); + kexinit.exchange.server_ephemeral.clear(); + kexinit + } + + pub fn initiate_rekey(ex: Exchange, session_id: &crate::Sha256Hash) -> Self { + let mut kexinit = KexInit { + exchange: ex, + algo: None, + sent: true, + session_id: Some(session_id.clone()), + }; + kexinit.exchange.client_kex_init.clear(); + kexinit.exchange.server_kex_init.clear(); + kexinit.exchange.client_ephemeral.clear(); + kexinit.exchange.server_ephemeral.clear(); + kexinit + } +} + +#[derive(Debug)] +pub struct KexDh { + pub exchange: Exchange, + pub names: negotiation::Names, + pub key: usize, + pub session_id: Option, +} + +#[derive(Debug)] +pub struct KexDhDone { + pub exchange: Exchange, + pub kex: kex::Algorithm, + pub key: usize, + pub session_id: Option, + pub names: negotiation::Names, +} + +impl KexDhDone { + pub fn compute_keys( + self, + hash: crate::Sha256Hash, + is_server: bool, + ) -> Result { + let session_id = if let Some(session_id) = self.session_id { + session_id + } else { + hash.clone() + }; + // Now computing keys. + let c = self + .kex + .compute_keys(&session_id, &hash, self.names.cipher, is_server)?; + Ok(NewKeys { + exchange: self.exchange, + names: self.names, + kex: self.kex, + key: self.key, + cipher: c, + session_id: session_id, + received: false, + sent: false, + }) + } +} + +#[derive(Debug)] +pub struct NewKeys { + pub exchange: Exchange, + pub names: negotiation::Names, + pub kex: kex::Algorithm, + pub key: usize, + pub cipher: cipher::CipherPair, + pub session_id: crate::Sha256Hash, + pub received: bool, + pub sent: bool, +} diff --git a/thrussh/src/ssh_read.rs b/thrussh/src/ssh_read.rs new file mode 100644 index 00000000..7504b868 --- /dev/null +++ b/thrussh/src/ssh_read.rs @@ -0,0 +1,163 @@ +use crate::Error; +use cryptovec::CryptoVec; +use futures::task::*; +use std; +use std::pin::Pin; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, ReadBuf}; + +/// The buffer to read the identification string (first line in the +/// protocol). +struct ReadSshIdBuffer { + pub buf: CryptoVec, + pub total: usize, + pub bytes_read: usize, + pub sshid_len: usize, +} + +impl ReadSshIdBuffer { + pub fn id(&self) -> &[u8] { + &self.buf[..self.sshid_len] + } + + pub fn new() -> ReadSshIdBuffer { + let mut buf = CryptoVec::new(); + buf.resize(256); + ReadSshIdBuffer { + buf: buf, + sshid_len: 0, + bytes_read: 0, + total: 0, + } + } +} + +impl std::fmt::Debug for ReadSshIdBuffer { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "ReadSshId {:?}", self.id()) + } +} + +/// SshRead is the same as R, plus a small buffer in the beginning to +/// read the identification string. After the first line in the +/// connection, the `id` parameter is never used again. +pub struct SshRead { + id: Option, + pub r: R, +} + +impl SshRead { + pub fn split(self) -> (SshRead>, tokio::io::WriteHalf) { + let (r, w) = tokio::io::split(self.r); + (SshRead { id: self.id, r }, w) + } +} + +impl AsyncRead for SshRead { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut ReadBuf, + ) -> Poll> { + if let Some(mut id) = self.id.take() { + debug!("id {:?} {:?}", id.total, id.bytes_read); + if id.total > id.bytes_read { + let total = id.total.min(id.bytes_read + buf.remaining()); + let result = { buf.put_slice(&id.buf[id.bytes_read..total]) }; + debug!("read {:?} bytes from id.buf", result); + id.bytes_read += total - id.bytes_read; + self.id = Some(id); + return Poll::Ready(Ok(())); + } + } + AsyncRead::poll_read(Pin::new(&mut self.get_mut().r), cx, buf) + } +} + +impl std::io::Write for SshRead { + fn write(&mut self, buf: &[u8]) -> Result { + self.r.write(buf) + } + fn flush(&mut self) -> Result<(), std::io::Error> { + self.r.flush() + } +} + +impl AsyncWrite for SshRead { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + AsyncWrite::poll_write(Pin::new(&mut self.r), cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + AsyncWrite::poll_flush(Pin::new(&mut self.r), cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context, + ) -> Poll> { + AsyncWrite::poll_shutdown(Pin::new(&mut self.r), cx) + } +} + +impl SshRead { + pub fn new(r: R) -> Self { + SshRead { + id: Some(ReadSshIdBuffer::new()), + r, + } + } + + pub async fn read_ssh_id(&mut self) -> Result<&[u8], Error> { + let ssh_id = self.id.as_mut().unwrap(); + loop { + let mut i = 0; + debug!("read_ssh_id: reading"); + let n = AsyncReadExt::read(&mut self.r, &mut ssh_id.buf[ssh_id.total..]).await?; + debug!("read {:?}", n); + + ssh_id.total += n; + debug!("{:?}", std::str::from_utf8(&ssh_id.buf[..ssh_id.total])); + if n == 0 { + return Err(Error::Disconnect.into()); + } + loop { + if i >= ssh_id.total - 1 { + break; + } + if ssh_id.buf[i] == b'\r' && ssh_id.buf[i + 1] == b'\n' { + ssh_id.bytes_read = i + 2; + break; + } else if ssh_id.buf[i + 1] == b'\n' { + // This is really wrong, but OpenSSH 7.4 uses + // it. + ssh_id.bytes_read = i + 2; + i += 1; + break; + } else { + i += 1; + } + } + + if ssh_id.bytes_read > 0 { + // If we have a full line, handle it. + if i >= 8 { + if &ssh_id.buf[0..8] == b"SSH-2.0-" { + // Either the line starts with "SSH-2.0-" + ssh_id.sshid_len = i; + return Ok(&ssh_id.buf[..ssh_id.sshid_len]); + } + } + // Else, it is a "preliminary" (see + // https://tools.ietf.org/html/rfc4253#section-4.2), + // and we can discard it and read the next one. + ssh_id.total = 0; + ssh_id.bytes_read = 0; + } + debug!("bytes_read: {:?}", ssh_id.bytes_read); + } + } +} diff --git a/thrussh/src/sshbuffer.rs b/thrussh/src/sshbuffer.rs new file mode 100644 index 00000000..dd96ff8f --- /dev/null +++ b/thrussh/src/sshbuffer.rs @@ -0,0 +1,44 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +use super::*; +use std::num::Wrapping; + +#[derive(Debug)] +pub struct SSHBuffer { + pub buffer: CryptoVec, + pub len: usize, // next packet length. + pub bytes: usize, + // Sequence numbers are on 32 bits and wrap. + // https://tools.ietf.org/html/rfc4253#section-6.4 + pub seqn: Wrapping, +} + +impl SSHBuffer { + pub fn new() -> Self { + SSHBuffer { + buffer: CryptoVec::new(), + len: 0, + bytes: 0, + seqn: Wrapping(0), + } + } + + pub fn send_ssh_id(&mut self, id: &[u8]) { + self.buffer.extend(id); + self.buffer.push(b'\r'); + self.buffer.push(b'\n'); + } +}