diff --git a/.all-contributorsrc b/.all-contributorsrc
index 1db94a0b..66444bad 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -175,6 +175,150 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "Nurrl",
+ "name": "Maya the bee",
+ "avatar_url": "https://avatars.githubusercontent.com/u/15341887?v=4",
+ "profile": "https://github.com/Nurrl",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "mmirate",
+ "name": "Milo Mirate",
+ "avatar_url": "https://avatars.githubusercontent.com/u/992859?v=4",
+ "profile": "https://github.com/mmirate",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "george-hopkins",
+ "name": "George Hopkins",
+ "avatar_url": "https://avatars.githubusercontent.com/u/552590?v=4",
+ "profile": "https://github.com/george-hopkins",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "akeamc",
+ "name": "Åke Amcoff",
+ "avatar_url": "https://avatars.githubusercontent.com/u/17624114?v=4",
+ "profile": "https://amcoff.net/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "bho01",
+ "name": "Brendon Ho",
+ "avatar_url": "https://avatars.githubusercontent.com/u/12106620?v=4",
+ "profile": "http://brendonho.com",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "samuela",
+ "name": "Samuel Ainsworth",
+ "avatar_url": "https://avatars.githubusercontent.com/u/226872?v=4",
+ "profile": "http://samlikes.pizza/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "sherlock-holo",
+ "name": "Sherlock Holo",
+ "avatar_url": "https://avatars.githubusercontent.com/u/10096425?v=4",
+ "profile": "https://github.com/Sherlock-Holo",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "ricott1",
+ "name": "Alessandro Ricottone",
+ "avatar_url": "https://avatars.githubusercontent.com/u/16502243?v=4",
+ "profile": "https://github.com/ricott1",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "T0b1-iOS",
+ "name": "T0b1-iOS",
+ "avatar_url": "https://avatars.githubusercontent.com/u/15174814?v=4",
+ "profile": "https://github.com/T0b1-iOS",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "shoaibmerchant",
+ "name": "Shoaib Merchant",
+ "avatar_url": "https://avatars.githubusercontent.com/u/4598631?v=4",
+ "profile": "https://mecha.so",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "gleason-m",
+ "name": "Michael Gleason",
+ "avatar_url": "https://avatars.githubusercontent.com/u/86493344?v=4",
+ "profile": "https://github.com/gleason-m",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "elegaanz",
+ "name": "Ana Gelez",
+ "avatar_url": "https://avatars.githubusercontent.com/u/16254623?v=4",
+ "profile": "https://ana.gelez.xyz",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "tomknig",
+ "name": "Tom König",
+ "avatar_url": "https://avatars.githubusercontent.com/u/3586316?v=4",
+ "profile": "https://github.com/tomknig",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Barre",
+ "name": "Pierre Barre",
+ "avatar_url": "https://avatars.githubusercontent.com/u/45085843?v=4",
+ "profile": "https://www.legaltile.com/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "spoutn1k",
+ "name": "Jean-Baptiste Skutnik",
+ "avatar_url": "https://avatars.githubusercontent.com/u/22240065?v=4",
+ "profile": "http://skutnik.page",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "packetsource",
+ "name": "Adam Chappell",
+ "avatar_url": "https://avatars.githubusercontent.com/u/6276475?v=4",
+ "profile": "http://blog.packetsource.net/",
+ "contributions": [
+ "code"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..c236b04c
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+github: eugeny
+open_collective: tabby
+ko_fi: eugeny
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 66ef82a8..3174f403 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -2,9 +2,9 @@ name: Rust
on:
push:
- branches: [ master ]
+ branches: [ main ]
pull_request:
- branches: [ master ]
+ branches: [ main ]
env:
CARGO_TERM_COLOR: always
@@ -27,6 +27,18 @@ jobs:
with:
package: russh
+ Formatting:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Install rustfmt
+ run: rustup component add rustfmt
+
+ - name: rustfmt
+ run: cargo fmt --check
+
Clippy:
runs-on: ubuntu-latest
diff --git a/Cargo.toml b/Cargo.toml
index d4ed211c..8ba84e34 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,8 +1,26 @@
[workspace]
-members = [ "russh-keys", "russh", "russh-config", "cryptovec"]
+members = ["russh-keys", "russh", "russh-config", "cryptovec", "pageant"]
[patch.crates-io]
russh = { path = "russh" }
russh-keys = { path = "russh-keys" }
russh-cryptovec = { path = "cryptovec" }
russh-config = { path = "russh-config" }
+
+[workspace.dependencies]
+aes = "0.8"
+async-trait = "0.1"
+byteorder = "1.4"
+digest = "0.10"
+futures = "0.3"
+hmac = "0.12"
+log = "0.4"
+openssl = { version = "0.10" }
+rand = "0.8"
+sha1 = { version = "0.10", features = ["oid"] }
+sha2 = { version = "0.10", features = ["oid"] }
+ssh-encoding = "0.2"
+ssh-key = { version = "0.6", features = ["ed25519", "rsa", "encryption"] }
+thiserror = "1.0"
+tokio = { version = "1.17.0" }
+tokio-stream = { version = "0.1", features = ["net", "sync"] }
diff --git a/README.md b/README.md
index 76346d74..44b5dd36 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,13 @@
# Russh
+
[![Rust](https://github.com/warp-tech/russh/actions/workflows/rust.yml/badge.svg)](https://github.com/warp-tech/russh/actions/workflows/rust.yml)
-[![All Contributors](https://img.shields.io/badge/all_contributors-19-orange.svg?style=flat-square)](#contributors-)
+[![All Contributors](https://img.shields.io/badge/all_contributors-35-orange.svg?style=flat-square)](#contributors-)
Low-level Tokio SSH2 client and server implementation.
+Examples: [simple client](russh/examples/client_exec_simple.rs), [interactive PTY client](russh/examples/client_exec_interactive.rs), [server](russh/examples/echoserver.rs), [SFTP client](russh/examples/sftp_client.rs), [SFTP server](russh/examples/sftp_server.rs).
+
This is a fork of [Thrussh](https://nest.pijul.com/pijul/thrussh) by Pierre-Étienne Meunier.
> ✨ = added in Russh
@@ -14,17 +17,26 @@ This is a fork of [Thrussh](https://nest.pijul.com/pijul/thrussh) by Pierre-Éti
* `direct-tcpip` (local port forwarding)
* `forward-tcpip` (remote port forwarding) ✨
* `direct-streamlocal` (local UNIX socket forwarding, client only) ✨
+* `forward-streamlocal` (remote UNIX socket forwarding) ✨
* Ciphers:
* `chacha20-poly1305@openssh.com`
* `aes256-gcm@openssh.com` ✨
* `aes256-ctr` ✨
* `aes192-ctr` ✨
* `aes128-ctr` ✨
+ * `aes256-cbc` ✨
+ * `aes192-cbc` ✨
+ * `aes128-cbc` ✨
+ * `3des-cbc` ✨
* Key exchanges:
* `curve25519-sha256@libssh.org`
* `diffie-hellman-group1-sha1` ✨
* `diffie-hellman-group14-sha1` ✨
* `diffie-hellman-group14-sha256` ✨
+ * `diffie-hellman-group16-sha512` ✨
+ * `ecdh-sha2-nistp256` ✨
+ * `ecdh-sha2-nistp384` ✨
+ * `ecdh-sha2-nistp521` ✨
* MACs:
* `hmac-sha1` ✨
* `hmac-sha2-256` ✨
@@ -32,15 +44,25 @@ This is a fork of [Thrussh](https://nest.pijul.com/pijul/thrussh) by Pierre-Éti
* `hmac-sha1-etm@openssh.com` ✨
* `hmac-sha2-256-etm@openssh.com` ✨
* `hmac-sha2-512-etm@openssh.com` ✨
-* Host keys:
+* Host keys and public key auth:
* `ssh-ed25519`
* `rsa-sha2-256`
* `rsa-sha2-512`
* `ssh-rsa` ✨
+ * `ecdsa-sha2-nistp256` ✨
+ * `ecdsa-sha2-nistp384` ✨
+ * `ecdsa-sha2-nistp521` ✨
+* Authentication methods:
+ * `password`
+ * `publickey`
+ * `keyboard-interactive`
+ * `none`
+ * OpenSSH certificates (client only ✨)
* Dependency updates
* OpenSSH keepalive request handling ✨
* OpenSSH agent forwarding channels ✨
* OpenSSH `server-sig-algs` extension ✨
+* `openssl` dependency is optional ✨
## Safety
@@ -60,7 +82,7 @@ This is a fork of [Thrussh](https://nest.pijul.com/pijul/thrussh) by Pierre-Éti
## Ecosystem
-* [russh-sftp](https://crates.io/crates/russh-sftp) - server-side SFTP subsystem support for `russh` - see `russh/examples/sftp_server.rs`.
+* [russh-sftp](https://crates.io/crates/russh-sftp) - server-side and client-side SFTP subsystem support for `russh` - see `russh/examples/sftp_server.rs` or `russh/examples/sftp_client.rs`.
* [async-ssh2-tokio](https://crates.io/crates/async-ssh2-tokio) - simple high-level API for running commands over SSH.
## Contributors ✨
@@ -96,6 +118,26 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Saksham Mittal 💻 |
Lucas Kent 💻 |
Raphael Druon 💻 |
+ Maya the bee 💻 |
+ Milo Mirate 💻 |
+
+
+ George Hopkins 💻 |
+ Åke Amcoff 💻 |
+ Brendon Ho 💻 |
+ Samuel Ainsworth 💻 |
+ Sherlock Holo 💻 |
+ Alessandro Ricottone 💻 |
+ T0b1-iOS 💻 |
+
+
+ Shoaib Merchant 💻 |
+ Michael Gleason 💻 |
+ Ana Gelez 💻 |
+ Tom König 💻 |
+ Pierre Barre 💻 |
+ Jean-Baptiste Skutnik 💻 |
+ Adam Chappell 💻 |
diff --git a/cryptovec/Cargo.toml b/cryptovec/Cargo.toml
index 92450230..e04cd6f5 100644
--- a/cryptovec/Cargo.toml
+++ b/cryptovec/Cargo.toml
@@ -7,8 +7,11 @@ include = ["Cargo.toml", "src/lib.rs"]
license = "Apache-2.0"
name = "russh-cryptovec"
repository = "https://github.com/warp-tech/russh"
-version = "0.7.0"
+version = "0.7.3"
+rust-version = "1.60"
[dependencies]
libc = "0.2"
+
+[target.'cfg(target_os = "windows")'.dependencies]
winapi = {version = "0.3", features = ["basetsd", "minwindef", "memoryapi"]}
diff --git a/cryptovec/src/lib.rs b/cryptovec/src/lib.rs
index 8ecd1f0d..256b7fc2 100644
--- a/cryptovec/src/lib.rs
+++ b/cryptovec/src/lib.rs
@@ -248,7 +248,15 @@ impl CryptoVec {
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);
+ let new_ptr = std::alloc::alloc_zeroed(next_layout);
+ if new_ptr.is_null() {
+ #[allow(clippy::panic)]
+ {
+ panic!("Realloc failed, pointer = {:?} {:?}", self, size)
+ }
+ }
+
+ self.p = new_ptr;
mlock(self.p, next_capacity);
if self.capacity > 0 {
@@ -261,15 +269,8 @@ impl CryptoVec {
std::alloc::dealloc(old_ptr, layout);
}
- if self.p.is_null() {
- #[allow(clippy::panic)]
- {
- panic!("Realloc failed, pointer = {:?} {:?}", self, size)
- }
- } else {
- self.capacity = next_capacity;
- self.size = size;
- }
+ self.capacity = next_capacity;
+ self.size = size;
}
}
}
@@ -429,3 +430,23 @@ impl Drop for CryptoVec {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // If `resize` is called with a size that is too large to be allocated, it
+ // should panic, and not segfault or fail silently.
+ #[test]
+ fn large_resize_panics() {
+ let result = std::panic::catch_unwind(|| {
+ let mut vec = CryptoVec::new();
+ // Write something into the vector, so that there is something to
+ // copy when reallocating, to test all code paths.
+ vec.push(42);
+
+ vec.resize(1_000_000_000_000)
+ });
+ assert!(result.is_err());
+ }
+}
diff --git a/pageant/Cargo.toml b/pageant/Cargo.toml
new file mode 100644
index 00000000..2622b465
--- /dev/null
+++ b/pageant/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+authors = ["Eugene "]
+description = "Pageant SSH agent transport client."
+documentation = "https://docs.rs/pageant"
+edition = "2018"
+license = "Apache-2.0"
+name = "pageant"
+repository = "https://github.com/warp-tech/russh"
+version = "0.0.1-beta.3"
+rust-version = "1.65"
+
+[dependencies]
+futures = { workspace = true }
+thiserror = { workspace = true }
+rand = { workspace = true }
+tokio = { workspace = true, features = ["io-util", "rt"] }
+bytes = "1.7"
+delegate = "0.12"
+
+[target.'cfg(windows)'.dependencies]
+windows = { version = "0.58", features = [
+ "Win32_UI_WindowsAndMessaging",
+ "Win32_System_Memory",
+ "Win32_Security",
+ "Win32_System_Threading",
+ "Win32_System_DataExchange",
+] }
diff --git a/pageant/src/lib.rs b/pageant/src/lib.rs
new file mode 100644
index 00000000..7af6c0c6
--- /dev/null
+++ b/pageant/src/lib.rs
@@ -0,0 +1,11 @@
+//! # Pageant SSH agent transport protocol implementation
+//!
+//! This crate provides a [PageantStream] type that implements [AsyncRead] and [AsyncWrite] traits and can be used to talk to a running Pageant instance.
+//!
+//! This crate only implements the transport, not the actual SSH agent protocol.
+
+#[cfg(windows)]
+mod pageant_impl;
+
+#[cfg(windows)]
+pub use pageant_impl::*;
diff --git a/pageant/src/pageant_impl.rs b/pageant/src/pageant_impl.rs
new file mode 100644
index 00000000..a02fe0a1
--- /dev/null
+++ b/pageant/src/pageant_impl.rs
@@ -0,0 +1,285 @@
+use std::io::IoSlice;
+use std::mem::size_of;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+use bytes::BytesMut;
+use delegate::delegate;
+use thiserror::Error;
+use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf};
+use windows::core::HSTRING;
+use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, INVALID_HANDLE_VALUE, LPARAM, WPARAM};
+use windows::Win32::Security::{
+ GetTokenInformation, InitializeSecurityDescriptor, SetSecurityDescriptorOwner, TokenUser,
+ PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR, TOKEN_QUERY, TOKEN_USER,
+};
+use windows::Win32::System::DataExchange::COPYDATASTRUCT;
+use windows::Win32::System::Memory::{
+ CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS,
+ PAGE_READWRITE,
+};
+use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
+use windows::Win32::UI::WindowsAndMessaging::{FindWindowW, SendMessageA, WM_COPYDATA};
+
+#[derive(Error, Debug)]
+pub enum Error {
+ #[error("Pageant not found")]
+ NotFound,
+
+ #[error("Buffer overflow")]
+ Overflow,
+
+ #[error("No response from Pageant")]
+ NoResponse,
+
+ #[error(transparent)]
+ WindowsError(#[from] windows::core::Error),
+}
+
+impl Error {
+ fn from_win32() -> Self {
+ Self::WindowsError(windows::core::Error::from_win32())
+ }
+}
+
+/// Pageant transport stream. Implements [AsyncRead] and [AsyncWrite].
+///
+/// The stream has a unique cookie and requests made in the same stream are considered the same "session".
+pub struct PageantStream {
+ stream: DuplexStream,
+}
+
+impl PageantStream {
+ pub fn new() -> Self {
+ let (one, mut two) = tokio::io::duplex(_AGENT_MAX_MSGLEN * 100);
+
+ let cookie = rand::random::().to_string();
+ tokio::spawn(async move {
+ let mut buf = BytesMut::new();
+ while let Ok(n) = two.read_buf(&mut buf).await {
+ if n == 0 {
+ break;
+ }
+ let msg = buf.split().freeze();
+ let response = query_pageant_direct(cookie.clone(), &msg).unwrap();
+ two.write_all(&response).await?
+ }
+ std::io::Result::Ok(())
+ });
+
+ Self { stream: one }
+ }
+}
+
+impl Default for PageantStream {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl AsyncRead for PageantStream {
+ delegate! {
+ to Pin::new(&mut self.stream) {
+ fn poll_read(
+ mut self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut ReadBuf<'_>,
+ ) -> Poll>;
+
+ }
+ }
+}
+
+impl AsyncWrite for PageantStream {
+ delegate! {
+ to Pin::new(&mut self.stream) {
+ fn poll_write(
+ mut self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &[u8],
+ ) -> Poll>;
+
+ fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>;
+
+ fn poll_write_vectored(
+ mut self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ bufs: &[IoSlice<'_>],
+ ) -> Poll>;
+
+ fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>;
+ }
+
+ to Pin::new(&self.stream) {
+ fn is_write_vectored(&self) -> bool;
+ }
+ }
+}
+
+struct MemoryMap {
+ filemap: HANDLE,
+ view: MEMORY_MAPPED_VIEW_ADDRESS,
+ length: usize,
+ pos: usize,
+}
+
+impl MemoryMap {
+ fn new(
+ name: String,
+ length: usize,
+ security_attributes: Option,
+ ) -> Result {
+ let filemap = unsafe {
+ CreateFileMappingW(
+ INVALID_HANDLE_VALUE,
+ security_attributes.map(|sa| &sa as *const _),
+ PAGE_READWRITE,
+ 0,
+ length as u32,
+ &HSTRING::from(name.clone()),
+ )
+ }?;
+ if filemap.is_invalid() {
+ return Err(Error::from_win32());
+ }
+ let view = unsafe { MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) };
+ Ok(Self {
+ filemap,
+ view,
+ length,
+ pos: 0,
+ })
+ }
+
+ fn seek(&mut self, pos: usize) {
+ self.pos = pos;
+ }
+
+ fn write(&mut self, data: &[u8]) -> Result<(), Error> {
+ if self.pos + data.len() > self.length {
+ return Err(Error::Overflow);
+ }
+
+ unsafe {
+ std::ptr::copy_nonoverlapping(
+ &data[0] as *const u8,
+ self.view.Value.add(self.pos) as *mut u8,
+ data.len(),
+ );
+ }
+ self.pos += data.len();
+ Ok(())
+ }
+
+ fn read(&mut self, n: usize) -> Vec {
+ let out = vec![0; n];
+ unsafe {
+ std::ptr::copy_nonoverlapping(
+ self.view.Value.add(self.pos) as *const u8,
+ out.as_ptr() as *mut u8,
+ n,
+ );
+ }
+ self.pos += n;
+ out
+ }
+}
+
+impl Drop for MemoryMap {
+ fn drop(&mut self) {
+ unsafe {
+ let _ = UnmapViewOfFile(self.view);
+ let _ = CloseHandle(self.filemap);
+ }
+ }
+}
+
+fn find_pageant_window() -> Result {
+ let w = unsafe { FindWindowW(&HSTRING::from("Pageant"), &HSTRING::from("Pageant")) }?;
+ if w.is_invalid() {
+ return Err(Error::NotFound);
+ }
+ Ok(w)
+}
+
+const _AGENT_COPYDATA_ID: u64 = 0x804E50BA;
+const _AGENT_MAX_MSGLEN: usize = 8192;
+
+pub fn is_pageant_running() -> bool {
+ find_pageant_window().is_ok()
+}
+
+/// Send a one-off query to Pageant and return a response.
+pub fn query_pageant_direct(cookie: String, msg: &[u8]) -> Result, Error> {
+ let hwnd = find_pageant_window()?;
+ let map_name = format!("PageantRequest{cookie}");
+
+ let user = unsafe {
+ let mut process_token = HANDLE::default();
+ OpenProcessToken(
+ GetCurrentProcess(),
+ TOKEN_QUERY,
+ &mut process_token as *mut _,
+ )?;
+
+ let mut info_size = 0;
+ let _ = GetTokenInformation(process_token, TokenUser, None, 0, &mut info_size);
+
+ let mut buffer = vec![0; info_size as usize];
+ GetTokenInformation(
+ process_token,
+ TokenUser,
+ Some(buffer.as_mut_ptr() as *mut _),
+ buffer.len() as u32,
+ &mut info_size,
+ )?;
+ let user: TOKEN_USER = *(buffer.as_ptr() as *const _);
+ let _ = CloseHandle(process_token);
+ user
+ };
+
+ let mut sd = SECURITY_DESCRIPTOR::default();
+ let sa = SECURITY_ATTRIBUTES {
+ lpSecurityDescriptor: &mut sd as *mut _ as *mut _,
+ bInheritHandle: true.into(),
+ ..Default::default()
+ };
+
+ let psd = PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut _);
+
+ unsafe {
+ InitializeSecurityDescriptor(psd, 1)?;
+ SetSecurityDescriptorOwner(psd, user.User.Sid, false)?;
+ }
+
+ let mut map: MemoryMap = MemoryMap::new(map_name.clone(), _AGENT_MAX_MSGLEN, Some(sa))?;
+ map.write(msg)?;
+
+ let mut char_buffer = map_name.as_bytes().to_vec();
+ char_buffer.push(0);
+ let cds = COPYDATASTRUCT {
+ dwData: _AGENT_COPYDATA_ID as usize,
+ cbData: char_buffer.len() as u32,
+ lpData: char_buffer.as_ptr() as *mut _,
+ };
+
+ let response = unsafe {
+ SendMessageA(
+ hwnd,
+ WM_COPYDATA,
+ WPARAM(size_of::()),
+ LPARAM(&cds as *const _ as isize),
+ )
+ };
+
+ if response.0 == 0 {
+ return Err(Error::NoResponse);
+ }
+
+ map.seek(0);
+ let mut buf = map.read(4);
+ let size = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
+ buf.extend(map.read(size));
+
+ Ok(buf)
+}
diff --git a/russh-config/Cargo.toml b/russh-config/Cargo.toml
index 1ad2846b..0f776b96 100644
--- a/russh-config/Cargo.toml
+++ b/russh-config/Cargo.toml
@@ -7,12 +7,14 @@ include = ["Cargo.toml", "src/lib.rs", "src/proxy.rs"]
license = "Apache-2.0"
name = "russh-config"
repository = "https://github.com/warp-tech/russh"
-version = "0.7.0"
+version = "0.7.1"
+rust-version = "1.65"
[dependencies]
-dirs-next = "2.0"
-futures = "0.3"
-log = "0.4"
-thiserror = "1.0"
-tokio = {version = "1.0", features = ["io-util", "net", "macros", "process"]}
+home = "0.5"
+futures = { workspace = true }
+globset = "0.4.14"
+log = { workspace = true }
+thiserror = { workspace = true }
+tokio = { workspace = true, features = ["io-util", "net", "macros", "process"] }
whoami = "1.2"
diff --git a/russh-config/src/lib.rs b/russh-config/src/lib.rs
index cdf4d95a..2dec9406 100644
--- a/russh-config/src/lib.rs
+++ b/russh-config/src/lib.rs
@@ -8,6 +8,7 @@ use std::io::Read;
use std::net::ToSocketAddrs;
use std::path::Path;
+use globset::Glob;
use log::debug;
use thiserror::*;
@@ -34,6 +35,7 @@ pub struct Config {
pub port: u16,
pub identity_file: Option,
pub proxy_command: Option,
+ pub proxy_jump: Option,
pub add_keys_to_agent: AddKeysToAgent,
}
@@ -45,22 +47,30 @@ impl Config {
port: 22,
identity_file: None,
proxy_command: None,
+ proxy_jump: 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));
- }
+ // Look for any of the ssh_config(5) percent-style tokens and expand them
+ // based on current data in the struct, returning a new String. This function
+ // can be employed late/lazy eg just before establishing a stream using ProxyCommand
+ // but also can be used to modify Hostname as config parse time
+ fn expand_tokens(&self, original: &str) -> String {
+ let mut string = original.to_string();
+ string = string.replace("%u", &self.user);
+ string = string.replace("%h", &self.host_name); // remote hostname (from context "host")
+ string = string.replace("%H", &self.host_name); // remote hostname (from context "host")
+ string = string.replace("%p", &format!("{}", self.port)); // original typed hostname (from context "host")
+ string = string.replace("%%", "%");
+ string
}
- pub async fn stream(&mut self) -> Result {
- self.update_proxy_command();
+ pub async fn stream(&self) -> Result {
if let Some(ref proxy_command) = self.proxy_command {
+ let proxy_command = self.expand_tokens(proxy_command);
let cmd: Vec<&str> = proxy_command.split(' ').collect();
Stream::proxy_command(cmd.first().unwrap_or(&""), cmd.get(1..).unwrap_or(&[]))
.await
@@ -76,7 +86,7 @@ impl Config {
}
pub fn parse_home(host: &str) -> Result {
- let mut home = if let Some(home) = dirs_next::home_dir() {
+ let mut home = if let Some(home) = home::home_dir() {
home
} else {
return Err(Error::NoHome);
@@ -93,26 +103,21 @@ pub fn parse_path>(path: P, host: &str) -> Result
parse(&s, host)
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum AddKeysToAgent {
Yes,
Confirm,
Ask,
+ #[default]
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 tokens = line.trim().splitn(2, ' ').collect::>();
+ if tokens.len() == 2 {
+ let (key, value) = (tokens.first().unwrap_or(&""), tokens.get(1).unwrap_or(&""));
let lower = key.to_lowercase();
if let Some(ref mut config) = config {
match lower.as_str() {
@@ -121,10 +126,7 @@ pub fn parse(file: &str, host: &str) -> Result {
config.user.clear();
config.user.push_str(value.trim_start());
}
- "hostname" => {
- config.host_name.clear();
- config.host_name.push_str(value.trim_start())
- }
+ "hostname" => config.host_name = config.expand_tokens(value.trim_start()),
"port" => {
if let Ok(port) = value.trim_start().parse() {
config.port = port
@@ -133,7 +135,7 @@ pub fn parse(file: &str, host: &str) -> Result {
"identityfile" => {
let id = value.trim_start();
if id.starts_with("~/") {
- if let Some(mut home) = dirs_next::home_dir() {
+ if let Some(mut home) = home::home_dir() {
home.push(id.split_at(2).1);
config.identity_file = Some(
home.to_str()
@@ -153,6 +155,7 @@ pub fn parse(file: &str, host: &str) -> Result {
}
}
"proxycommand" => config.proxy_command = Some(value.trim_start().to_string()),
+ "proxyjump" => config.proxy_jump = 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,
@@ -163,7 +166,11 @@ pub fn parse(file: &str, host: &str) -> Result {
debug!("{:?}", key);
}
}
- } else if lower.as_str() == "host" && value.trim_start() == host {
+ } else if lower.as_str() == "host"
+ && value
+ .split_whitespace()
+ .any(|x| check_host_against_glob_pattern(host, x))
+ {
let mut c = Config::default(host);
c.port = 22;
config = Some(c)
@@ -176,3 +183,10 @@ pub fn parse(file: &str, host: &str) -> Result {
Err(Error::HostNotFound)
}
}
+
+fn check_host_against_glob_pattern(candidate: &str, glob_pattern: &str) -> bool {
+ match Glob::new(glob_pattern) {
+ Ok(glob) => glob.compile_matcher().is_match(candidate),
+ _ => false,
+ }
+}
diff --git a/russh-keys/Cargo.toml b/russh-keys/Cargo.toml
index fd5bcc80..80fbb610 100644
--- a/russh-keys/Cargo.toml
+++ b/russh-keys/Cargo.toml
@@ -4,71 +4,76 @@ description = "Deal with SSH keys: load them, decrypt them, call an SSH agent."
documentation = "https://docs.rs/russh-keys"
edition = "2018"
homepage = "https://github.com/warp-tech/russh"
-include = [
- "Cargo.toml",
- "src/lib.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",
-]
keywords = ["ssh"]
license = "Apache-2.0"
name = "russh-keys"
repository = "https://github.com/warp-tech/russh"
-version = "0.37.1"
+version = "0.46.0-beta.3"
+rust-version = "1.65"
[dependencies]
-aes = "0.8"
-async-trait = "0.1.72"
+aes = { workspace = true }
+async-trait = { workspace = true }
bcrypt-pbkdf = "0.10"
-bit-vec = "0.6"
cbc = "0.1"
ctr = "0.9"
block-padding = { version = "0.3", features = ["std"] }
-byteorder = "1.4"
+byteorder = { workspace = true }
data-encoding = "2.3"
-dirs = "5.0"
-ed25519-dalek = { version= "2.0", features = ["rand_core"] }
-futures = "0.3"
-hmac = "0.12"
+digest = { workspace = true }
+der = "0.7"
+home = "0.5"
+ecdsa = "0.16"
+ed25519-dalek = { version = "2.0", features = ["rand_core", "pkcs8"] }
+elliptic-curve = "0.13"
+futures = { workspace = true }
+hmac = { workspace = true }
inout = { version = "0.1", features = ["std"] }
-log = "0.4"
+log = { workspace = true }
md5 = "0.7"
-num-bigint = "0.4"
num-integer = "0.1"
-openssl = { version = "0.10", optional = true }
+openssl = { workspace = true, optional = true }
+p256 = "0.13"
+p384 = "0.13"
+p521 = "0.13"
pbkdf2 = "0.11"
-rand = "0.7"
+pkcs1 = "0.7"
+pkcs5 = "0.7"
+pkcs8 = { version = "0.10", features = ["pkcs5", "encryption"] }
+rand = { workspace = true }
rand_core = { version = "0.6.4", features = ["std"] }
+rsa = "0.9"
russh-cryptovec = { version = "0.7.0", path = "../cryptovec" }
+sec1 = { version = "0.7", features = ["pkcs8"] }
serde = { version = "1.0", features = ["derive"] }
-sha2 = "0.10"
-thiserror = "1.0"
-tokio = { version = "1.17.0", features = [
+sha1 = { workspace = true }
+sha2 = { workspace = true }
+spki = "0.7"
+ssh-encoding = { workspace = true }
+ssh-key = { workspace = true }
+thiserror = { workspace = true }
+tokio = { workspace = true, features = [
"io-util",
"rt-multi-thread",
"time",
"net",
] }
-tokio-stream = { version = "0.1", features = ["net"] }
-yasna = { version = "0.5.0", features = ["bit-vec", "num-bigint"] }
+tokio-stream = { workspace = true }
+typenum = "1.17"
+yasna = { version = "0.5.0", features = ["bit-vec", "num-bigint"], optional = true }
+zeroize = "1.7"
[features]
vendored-openssl = ["openssl", "openssl/vendored"]
+legacy-ed25519-pkcs8-parser = ["yasna"]
+
+[target.'cfg(windows)'.dependencies]
+pageant = { version = "0.0.1-beta.3", path = "../pageant" }
[dev-dependencies]
env_logger = "0.10"
tempdir = "0.3"
+tokio = { workspace = true, features = ["test-util", "macros", "process"] }
[package.metadata.docs.rs]
features = ["openssl"]
diff --git a/russh-keys/src/agent/client.rs b/russh-keys/src/agent/client.rs
index 1e730030..82d78980 100644
--- a/russh-keys/src/agent/client.rs
+++ b/russh-keys/src/agent/client.rs
@@ -1,7 +1,7 @@
use std::convert::TryFrom;
use byteorder::{BigEndian, ByteOrder};
-use log::{debug, info};
+use log::debug;
use russh_cryptovec::CryptoVec;
use tokio;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
@@ -9,16 +9,35 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use super::{msg, Constraint};
use crate::encoding::{Encoding, Reader};
use crate::key::{PublicKey, SignatureHash};
-use crate::{key, Error};
+use crate::{key, protocol, Error, PublicKeyBase64};
+
+pub trait AgentStream: AsyncRead + AsyncWrite {}
+
+impl AgentStream for S {}
/// SSH agent client.
-pub struct AgentClient {
+pub struct AgentClient {
stream: S,
buf: CryptoVec,
}
+impl AgentClient {
+ /// Wraps the internal stream in a Box, allowing different client
+ /// implementations to have the same type
+ pub fn dynamic(self) -> AgentClient> {
+ AgentClient {
+ stream: Box::new(self.stream),
+ buf: self.buf,
+ }
+ }
+
+ pub fn into_inner(self) -> Box {
+ Box::new(self.stream)
+ }
+}
+
// https://tools.ietf.org/html/draft-miller-ssh-agent-00#section-4.1
-impl AgentClient {
+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 {
@@ -31,7 +50,7 @@ impl AgentClient {
#[cfg(unix)]
impl AgentClient {
- /// Build a future that connects to an SSH agent via the provided
+ /// Connect 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?;
@@ -41,8 +60,8 @@ impl AgentClient {
})
}
- /// Build a future that connects to an SSH agent via the provided
- /// stream (on Unix, usually a Unix-domain socket).
+ /// Connect to an SSH agent specified by the SSH_AUTH_SOCK
+ /// environment variable.
pub async fn connect_env() -> Result {
let var = if let Ok(var) = std::env::var("SSH_AUTH_SOCK") {
var
@@ -58,16 +77,39 @@ impl AgentClient {
}
}
-#[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)
+#[cfg(windows)]
+const ERROR_PIPE_BUSY: u32 = 231u32;
+
+#[cfg(windows)]
+impl AgentClient {
+ /// Connect to a running Pageant instance
+ pub async fn connect_pageant() -> Self {
+ Self::connect(pageant::PageantStream::new())
+ }
+}
+
+#[cfg(windows)]
+impl AgentClient {
+ /// Connect to an SSH agent via a Windows named pipe
+ pub async fn connect_named_pipe>(path: P) -> Result {
+ let stream = loop {
+ match tokio::net::windows::named_pipe::ClientOptions::new().open(path.as_ref()) {
+ Ok(client) => break client,
+ Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY as i32) => (),
+ Err(e) => return Err(e.into()),
+ }
+
+ tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
+ };
+
+ Ok(AgentClient {
+ stream,
+ buf: CryptoVec::new(),
+ })
}
}
-impl AgentClient {
+impl AgentClient {
async fn read_response(&mut self) -> Result<(), Error> {
// Writing the message
self.stream.write_all(&self.buf).await?;
@@ -87,6 +129,15 @@ impl AgentClient {
Ok(())
}
+ async fn read_success(&mut self) -> Result<(), Error> {
+ self.read_response().await?;
+ if self.buf.first() == Some(&msg::SUCCESS) {
+ Ok(())
+ } else {
+ Err(Error::AgentFailure)
+ }
+ }
+
/// 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(
@@ -94,6 +145,8 @@ impl AgentClient {
key: &key::KeyPair,
constraints: &[Constraint],
) -> Result<(), Error> {
+ // See IETF draft-miller-ssh-agent-13, section 3.2 for format.
+ // https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent
self.buf.clear();
self.buf.resize(4);
if constraints.is_empty() {
@@ -110,33 +163,27 @@ impl AgentClient {
self.buf.extend(pair.verifying_key().as_bytes());
self.buf.extend_ssh_string(b"");
}
- #[cfg(feature = "openssl")]
#[allow(clippy::unwrap_used)] // key is known to be private
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"");
+ self.buf
+ .extend_ssh(&protocol::RsaPrivateKey::try_from(key)?);
+ }
+ key::KeyPair::EC { ref key } => {
+ self.buf.extend_ssh_string(key.algorithm().as_bytes());
+ self.buf.extend_ssh_string(key.ident().as_bytes());
+ self.buf
+ .extend_ssh_string(&key.to_public_key().to_sec1_bytes());
+ self.buf.extend_ssh_mpint(&key.to_secret_bytes());
+ self.buf.extend_ssh_string(b""); // comment
}
}
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)
+ self.buf.push_u32_be(seconds);
}
Constraint::Confirm => self.buf.push(msg::CONSTRAIN_CONFIRM),
Constraint::Extensions {
@@ -153,7 +200,7 @@ impl AgentClient {
let len = self.buf.len() - 4;
BigEndian::write_u32(&mut self.buf[..], len as u32);
- self.read_response().await?;
+ self.read_success().await?;
Ok(())
}
@@ -243,34 +290,12 @@ impl AgentClient {
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" => keys.push(PublicKey::Ed25519(
- ed25519_dalek::VerifyingKey::try_from(r.read_string()?)?,
- )),
- t => {
- info!("Unsupported key type: {:?}", std::str::from_utf8(t))
- }
- }
+ let key_blob = r.read_string()?;
+ let _comment = r.read_string()?;
+ keys.push(key::parse_public_key(
+ key_blob,
+ Some(SignatureHash::SHA2_512),
+ )?);
}
}
@@ -322,7 +347,6 @@ impl AgentClient {
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,
@@ -467,8 +491,8 @@ impl AgentClient {
self.buf.clear();
self.buf.resize(4);
self.buf.push(msg::REMOVE_ALL_IDENTITIES);
- BigEndian::write_u32(&mut self.buf[..], 5);
- self.read_response().await?;
+ BigEndian::write_u32(&mut self.buf[..], 1);
+ self.read_success().await?;
Ok(())
}
@@ -505,14 +529,11 @@ impl AgentClient {
fn key_blob(public: &key::PublicKey, buf: &mut CryptoVec) -> Result<(), Error> {
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()?;
- buf.extend_ssh_mpint(&rsa.e().to_vec());
- buf.extend_ssh_mpint(&rsa.n().to_vec());
+ buf.extend_ssh(&protocol::RsaPublicKey::from(key));
let len1 = buf.len();
#[allow(clippy::indexing_slicing)] // length is known
BigEndian::write_u32(&mut buf[5..], (len1 - len0) as u32);
@@ -526,6 +547,9 @@ fn key_blob(public: &key::PublicKey, buf: &mut CryptoVec) -> Result<(), Error> {
#[allow(clippy::indexing_slicing)] // length is known
BigEndian::write_u32(&mut buf[5..], (len1 - len0) as u32);
}
+ PublicKey::EC { .. } => {
+ buf.extend_ssh_string(&public.public_key_bytes());
+ }
}
Ok(())
}
diff --git a/russh-keys/src/agent/msg.rs b/russh-keys/src/agent/msg.rs
index a77c5091..d732e674 100644
--- a/russh-keys/src/agent/msg.rs
+++ b/russh-keys/src/agent/msg.rs
@@ -19,4 +19,5 @@ pub const EXTENSION: u8 = 27;
pub const CONSTRAIN_LIFETIME: u8 = 1;
pub const CONSTRAIN_CONFIRM: u8 = 2;
-pub const CONSTRAIN_EXTENSION: u8 = 3;
+// pub const CONSTRAIN_MAXSIGN: u8 = 3;
+pub const CONSTRAIN_EXTENSION: u8 = 255;
diff --git a/russh-keys/src/agent/server.rs b/russh-keys/src/agent/server.rs
index c61a8e0d..be89509a 100644
--- a/russh-keys/src/agent/server.rs
+++ b/russh-keys/src/agent/server.rs
@@ -15,8 +15,6 @@ use {std, tokio};
use super::{msg, Constraint};
use crate::encoding::{Encoding, Position, Reader};
-#[cfg(feature = "openssl")]
-use crate::key::SignatureHash;
use crate::{key, Error};
#[derive(Clone)]
@@ -252,82 +250,27 @@ impl Result {
- let pos0 = r.position;
- let t = r.read_string()?;
- let (blob, key) = match t {
- b"ssh-ed25519" => {
- let pos1 = r.position;
- let concat = r.read_string()?;
- let _comment = r.read_string()?;
- #[allow(clippy::indexing_slicing)] // length checked before
- let secret = ed25519_dalek::SigningKey::try_from(
- concat.get(..32).ok_or(Error::KeyIsCorrupt)?,
- ).map_err(|_| Error::KeyIsCorrupt)?;
+ let (blob, key_pair) = {
+ use ssh_encoding::{Decode, Encode};
- writebuf.push(msg::SUCCESS);
+ let private_key = ssh_key::private::PrivateKey::new(
+ ssh_key::private::KeypairData::decode(&mut r)?,
+ "",
+ )?;
+ let _comment = r.read_string()?;
+ let key_pair = key::KeyPair::try_from(&private_key)?;
- #[allow(clippy::indexing_slicing)] // positions checked before
- (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 mut blob = Vec::new();
+ private_key.public_key().key_data().encode(&mut blob)?;
- #[allow(clippy::indexing_slicing)] // length is known
- 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),
+ (blob, key_pair)
};
+ writebuf.push(msg::SUCCESS);
let mut w = self.keys.0.write().or(Err(Error::AgentFailure))?;
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()?;
+ while let Ok(t) = r.read_byte() {
if t == msg::CONSTRAIN_LIFETIME {
let seconds = r.read_u32()?;
c.push(Constraint::KeyLifetime { seconds });
@@ -352,9 +295,9 @@ impl,
+ pkey: PKey,
+}
+
+impl RsaPublic {
+ pub fn verify_detached(&self, hash: &SignatureHash, msg: &[u8], sig: &[u8]) -> bool {
+ openssl::sign::Verifier::new(message_digest_for(hash), &self.pkey)
+ .and_then(|mut v| v.verify_oneshot(sig, msg))
+ .unwrap_or(false)
+ }
+}
+
+impl TryFrom<&protocol::RsaPublicKey<'_>> for RsaPublic {
+ type Error = Error;
+
+ fn try_from(pk: &protocol::RsaPublicKey<'_>) -> Result {
+ let key = Rsa::from_public_components(
+ BigNum::from_slice(&pk.modulus)?,
+ BigNum::from_slice(&pk.public_exponent)?,
+ )?;
+ Ok(Self {
+ pkey: PKey::from_rsa(key.clone())?,
+ key,
+ })
+ }
+}
+
+impl<'a> From<&RsaPublic> for protocol::RsaPublicKey<'a> {
+ fn from(key: &RsaPublic) -> Self {
+ Self {
+ modulus: key.key.n().to_vec().into(),
+ public_exponent: key.key.e().to_vec().into(),
+ }
+ }
+}
+
+impl PartialEq for RsaPublic {
+ fn eq(&self, b: &RsaPublic) -> bool {
+ self.pkey.public_eq(&b.pkey)
+ }
+}
+
+impl Eq for RsaPublic {}
+
+impl std::fmt::Debug for RsaPublic {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "RsaPublic {{ (hidden) }}")
+ }
+}
+
+#[derive(Clone)]
+pub struct RsaPrivate {
+ key: Rsa,
+ pkey: PKey,
+}
+
+impl RsaPrivate {
+ pub fn new(
+ sk: &protocol::RsaPrivateKey<'_>,
+ extra: Option<&RsaCrtExtra<'_>>,
+ ) -> Result {
+ let (d, p, q) = (
+ BigNum::from_slice(&sk.private_exponent)?,
+ BigNum::from_slice(&sk.prime1)?,
+ BigNum::from_slice(&sk.prime2)?,
+ );
+ let (dp, dq) = if let Some(extra) = extra {
+ (
+ BigNum::from_slice(&extra.dp)?,
+ BigNum::from_slice(&extra.dq)?,
+ )
+ } else {
+ calc_dp_dq(d.as_ref(), p.as_ref(), q.as_ref())?
+ };
+ let key = Rsa::from_private_components(
+ BigNum::from_slice(&sk.public_key.modulus)?,
+ BigNum::from_slice(&sk.public_key.public_exponent)?,
+ d,
+ p,
+ q,
+ dp,
+ dq,
+ BigNum::from_slice(&sk.coefficient)?,
+ )?;
+ key.check_key()?;
+ Ok(Self {
+ pkey: PKey::from_rsa(key.clone())?,
+ key,
+ })
+ }
+
+ pub fn new_from_der(der: &[u8]) -> Result {
+ let key = Rsa::private_key_from_der(der)?;
+ key.check_key()?;
+ Ok(Self {
+ pkey: PKey::from_rsa(key.clone())?,
+ key,
+ })
+ }
+
+ pub fn generate(bits: usize) -> Result {
+ let key = Rsa::generate(bits as u32)?;
+ Ok(Self {
+ pkey: PKey::from_rsa(key.clone())?,
+ key,
+ })
+ }
+
+ pub fn sign(&self, hash: &SignatureHash, msg: &[u8]) -> Result, Error> {
+ Ok(
+ openssl::sign::Signer::new(message_digest_for(hash), &self.pkey)?
+ .sign_oneshot_to_vec(msg)?,
+ )
+ }
+}
+
+impl<'a> TryFrom<&RsaPrivate> for protocol::RsaPrivateKey<'a> {
+ type Error = Error;
+
+ fn try_from(key: &RsaPrivate) -> Result, Self::Error> {
+ let key = &key.key;
+ // We always set these.
+ if let (Some(p), Some(q), Some(iqmp)) = (key.p(), key.q(), key.iqmp()) {
+ Ok(protocol::RsaPrivateKey {
+ public_key: protocol::RsaPublicKey {
+ modulus: key.n().to_vec().into(),
+ public_exponent: key.e().to_vec().into(),
+ },
+ private_exponent: key.d().to_vec().into(),
+ prime1: p.to_vec().into(),
+ prime2: q.to_vec().into(),
+ coefficient: iqmp.to_vec().into(),
+ comment: b"".as_slice().into(),
+ })
+ } else {
+ Err(Error::KeyIsCorrupt)
+ }
+ }
+}
+
+impl<'a> TryFrom<&RsaPrivate> for RsaCrtExtra<'a> {
+ type Error = Error;
+
+ fn try_from(key: &RsaPrivate) -> Result, Self::Error> {
+ let key = &key.key;
+ // We always set these.
+ if let (Some(dp), Some(dq)) = (key.dmp1(), key.dmq1()) {
+ Ok(RsaCrtExtra {
+ dp: dp.to_vec().into(),
+ dq: dq.to_vec().into(),
+ })
+ } else {
+ Err(Error::KeyIsCorrupt)
+ }
+ }
+}
+
+impl<'a> From<&RsaPrivate> for protocol::RsaPublicKey<'a> {
+ fn from(key: &RsaPrivate) -> Self {
+ Self {
+ modulus: key.key.n().to_vec().into(),
+ public_exponent: key.key.e().to_vec().into(),
+ }
+ }
+}
+
+impl TryFrom<&RsaPrivate> for RsaPublic {
+ type Error = Error;
+
+ fn try_from(key: &RsaPrivate) -> Result {
+ let key = Rsa::from_public_components(key.key.n().to_owned()?, key.key.e().to_owned()?)?;
+ Ok(Self {
+ pkey: PKey::from_rsa(key.clone())?,
+ key,
+ })
+ }
+}
+
+fn message_digest_for(hash: &SignatureHash) -> MessageDigest {
+ match hash {
+ SignatureHash::SHA2_256 => MessageDigest::sha256(),
+ SignatureHash::SHA2_512 => MessageDigest::sha512(),
+ SignatureHash::SHA1 => MessageDigest::sha1(),
+ }
+}
+
+fn calc_dp_dq(d: &BigNumRef, p: &BigNumRef, q: &BigNumRef) -> Result<(BigNum, BigNum), Error> {
+ let one = BigNum::from_u32(1)?;
+ let p1 = p - one.as_ref();
+ let q1 = q - 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)?;
+ Ok((dp, dq))
+}
diff --git a/russh-keys/src/backend_rust.rs b/russh-keys/src/backend_rust.rs
new file mode 100644
index 00000000..9b568887
--- /dev/null
+++ b/russh-keys/src/backend_rust.rs
@@ -0,0 +1,184 @@
+use std::convert::TryFrom;
+
+use rsa::traits::{PrivateKeyParts, PublicKeyParts};
+use rsa::BigUint;
+
+use crate::key::{RsaCrtExtra, SignatureHash};
+use crate::{protocol, Error};
+
+#[derive(Clone, PartialEq, Eq)]
+pub struct RsaPublic {
+ key: rsa::RsaPublicKey,
+}
+
+impl RsaPublic {
+ pub fn verify_detached(&self, hash: &SignatureHash, msg: &[u8], sig: &[u8]) -> bool {
+ self.key
+ .verify(signature_scheme_for_hash(hash), &hash_msg(hash, msg), sig)
+ .is_ok()
+ }
+}
+
+impl TryFrom<&protocol::RsaPublicKey<'_>> for RsaPublic {
+ type Error = Error;
+
+ fn try_from(pk: &protocol::RsaPublicKey<'_>) -> Result {
+ Ok(Self {
+ key: rsa::RsaPublicKey::new(
+ BigUint::from_bytes_be(&pk.modulus),
+ BigUint::from_bytes_be(&pk.public_exponent),
+ )?,
+ })
+ }
+}
+
+impl<'a> From<&RsaPublic> for protocol::RsaPublicKey<'a> {
+ fn from(key: &RsaPublic) -> Self {
+ Self {
+ modulus: key.key.n().to_bytes_be().into(),
+ public_exponent: key.key.e().to_bytes_be().into(),
+ }
+ }
+}
+
+impl std::fmt::Debug for RsaPublic {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "RsaPublic {{ (hidden) }}")
+ }
+}
+
+#[derive(Clone)]
+pub struct RsaPrivate {
+ key: rsa::RsaPrivateKey,
+}
+
+impl RsaPrivate {
+ pub fn new(
+ sk: &protocol::RsaPrivateKey<'_>,
+ extra: Option<&RsaCrtExtra<'_>>,
+ ) -> Result {
+ let mut key = rsa::RsaPrivateKey::from_components(
+ BigUint::from_bytes_be(&sk.public_key.modulus),
+ BigUint::from_bytes_be(&sk.public_key.public_exponent),
+ BigUint::from_bytes_be(&sk.private_exponent),
+ vec![
+ BigUint::from_bytes_be(&sk.prime1),
+ BigUint::from_bytes_be(&sk.prime2),
+ ],
+ )?;
+ key.validate()?;
+ key.precompute()?;
+
+ if Some(BigUint::from_bytes_be(&sk.coefficient)) != key.crt_coefficient() {
+ return Err(Error::KeyIsCorrupt);
+ }
+ if let Some(extra) = extra {
+ if (
+ Some(&BigUint::from_bytes_be(&extra.dp)),
+ Some(&BigUint::from_bytes_be(&extra.dq)),
+ ) != (key.dp(), key.dq())
+ {
+ return Err(Error::KeyIsCorrupt);
+ }
+ }
+
+ Ok(Self { key })
+ }
+
+ pub fn new_from_der(der: &[u8]) -> Result {
+ use pkcs1::DecodeRsaPrivateKey;
+ Ok(Self {
+ key: rsa::RsaPrivateKey::from_pkcs1_der(der)?,
+ })
+ }
+
+ pub fn generate(bits: usize) -> Result {
+ Ok(Self {
+ key: rsa::RsaPrivateKey::new(&mut crate::key::safe_rng(), bits)?,
+ })
+ }
+
+ pub fn sign(&self, hash: &SignatureHash, msg: &[u8]) -> Result, Error> {
+ Ok(self
+ .key
+ .sign(signature_scheme_for_hash(hash), &hash_msg(hash, msg))?)
+ }
+}
+
+impl<'a> TryFrom<&RsaPrivate> for protocol::RsaPrivateKey<'a> {
+ type Error = Error;
+
+ fn try_from(key: &RsaPrivate) -> Result, Self::Error> {
+ let key = &key.key;
+ // We always precompute these.
+ if let ([p, q], Some(iqmp)) = (key.primes(), key.crt_coefficient()) {
+ Ok(protocol::RsaPrivateKey {
+ public_key: protocol::RsaPublicKey {
+ modulus: key.n().to_bytes_be().into(),
+ public_exponent: key.e().to_bytes_be().into(),
+ },
+ private_exponent: key.d().to_bytes_be().into(),
+ prime1: p.to_bytes_be().into(),
+ prime2: q.to_bytes_be().into(),
+ coefficient: iqmp.to_bytes_be().into(),
+ comment: b"".as_slice().into(),
+ })
+ } else {
+ Err(Error::KeyIsCorrupt)
+ }
+ }
+}
+
+impl<'a> TryFrom<&RsaPrivate> for RsaCrtExtra<'a> {
+ type Error = Error;
+
+ fn try_from(key: &RsaPrivate) -> Result, Self::Error> {
+ let key = &key.key;
+ // We always precompute these.
+ if let (Some(dp), Some(dq)) = (key.dp(), key.dq()) {
+ Ok(RsaCrtExtra {
+ dp: dp.to_bytes_be().into(),
+ dq: dq.to_bytes_be().into(),
+ })
+ } else {
+ Err(Error::KeyIsCorrupt)
+ }
+ }
+}
+
+impl<'a> From<&RsaPrivate> for protocol::RsaPublicKey<'a> {
+ fn from(key: &RsaPrivate) -> Self {
+ Self {
+ modulus: key.key.n().to_bytes_be().into(),
+ public_exponent: key.key.e().to_bytes_be().into(),
+ }
+ }
+}
+
+impl TryFrom<&RsaPrivate> for RsaPublic {
+ type Error = Error;
+
+ fn try_from(key: &RsaPrivate) -> Result {
+ Ok(Self {
+ key: key.key.to_public_key(),
+ })
+ }
+}
+
+fn signature_scheme_for_hash(hash: &SignatureHash) -> rsa::pkcs1v15::Pkcs1v15Sign {
+ use rsa::pkcs1v15::Pkcs1v15Sign;
+ match *hash {
+ SignatureHash::SHA2_256 => Pkcs1v15Sign::new::(),
+ SignatureHash::SHA2_512 => Pkcs1v15Sign::new::(),
+ SignatureHash::SHA1 => Pkcs1v15Sign::new::(),
+ }
+}
+
+fn hash_msg(hash: &SignatureHash, msg: &[u8]) -> Vec {
+ use digest::Digest;
+ match *hash {
+ SignatureHash::SHA2_256 => sha2::Sha256::digest(msg).to_vec(),
+ SignatureHash::SHA2_512 => sha2::Sha512::digest(msg).to_vec(),
+ SignatureHash::SHA1 => sha1::Sha1::digest(msg).to_vec(),
+ }
+}
diff --git a/russh-keys/src/ec.rs b/russh-keys/src/ec.rs
new file mode 100644
index 00000000..689ad15a
--- /dev/null
+++ b/russh-keys/src/ec.rs
@@ -0,0 +1,263 @@
+use elliptic_curve::{Curve, CurveArithmetic, FieldBytes, FieldBytesSize};
+
+use crate::key::safe_rng;
+use crate::Error;
+
+// p521::{SigningKey, VerifyingKey} are wrapped versions and do not provide PartialEq and Eq, hence
+// we make our own type alias here.
+mod local_p521 {
+ use rand_core::CryptoRngCore;
+ use sha2::{Digest, Sha512};
+
+ pub type NistP521 = p521::NistP521;
+ pub type VerifyingKey = ecdsa::VerifyingKey;
+ pub type SigningKey = ecdsa::SigningKey;
+ pub type Signature = ecdsa::Signature;
+ pub type Result = ecdsa::Result;
+
+ // Implement signing because p521::NistP521 does not implement DigestPrimitive trait.
+ pub fn try_sign_with_rng(
+ key: &SigningKey,
+ rng: &mut impl CryptoRngCore,
+ msg: &[u8],
+ ) -> Result {
+ use ecdsa::hazmat::{bits2field, sign_prehashed};
+ use elliptic_curve::Field;
+ let prehash = Sha512::digest(msg);
+ let z = bits2field::(&prehash)?;
+ let k = p521::Scalar::random(rng);
+ sign_prehashed(key.as_nonzero_scalar().as_ref(), k, &z).map(|sig| sig.0)
+ }
+
+ // Implement verifying because ecdsa::VerifyingKey does not satisfy the trait
+ // bound requirements of the DigestVerifier's implementation in ecdsa crate.
+ pub fn verify(key: &VerifyingKey, msg: &[u8], signature: &Signature) -> Result<()> {
+ use ecdsa::signature::hazmat::PrehashVerifier;
+ key.verify_prehash(&Sha512::digest(msg), signature)
+ }
+}
+
+const CURVE_NISTP256: &str = "nistp256";
+const CURVE_NISTP384: &str = "nistp384";
+const CURVE_NISTP521: &str = "nistp521";
+
+/// An ECC public key.
+#[derive(Clone, Eq, PartialEq)]
+pub enum PublicKey {
+ P256(p256::ecdsa::VerifyingKey),
+ P384(p384::ecdsa::VerifyingKey),
+ P521(local_p521::VerifyingKey),
+}
+
+impl PublicKey {
+ /// Returns the elliptic curve domain parameter identifiers defined in RFC 5656 section 6.1.
+ pub fn ident(&self) -> &'static str {
+ match self {
+ Self::P256(_) => CURVE_NISTP256,
+ Self::P384(_) => CURVE_NISTP384,
+ Self::P521(_) => CURVE_NISTP521,
+ }
+ }
+
+ /// Returns the ECC public key algorithm name defined in RFC 5656 section 6.2, in the form of
+ /// `"ecdsa-sha2-[identifier]"`.
+ pub fn algorithm(&self) -> &'static str {
+ match self {
+ Self::P256(_) => crate::ECDSA_SHA2_NISTP256,
+ Self::P384(_) => crate::ECDSA_SHA2_NISTP384,
+ Self::P521(_) => crate::ECDSA_SHA2_NISTP521,
+ }
+ }
+
+ /// Creates a `PrivateKey` from algorithm name and SEC1-encoded point on curve.
+ pub fn from_sec1_bytes(algorithm: &[u8], bytes: &[u8]) -> Result {
+ match algorithm {
+ crate::KEYTYPE_ECDSA_SHA2_NISTP256 => Ok(Self::P256(
+ p256::ecdsa::VerifyingKey::from_sec1_bytes(bytes)?,
+ )),
+ crate::KEYTYPE_ECDSA_SHA2_NISTP384 => Ok(Self::P384(
+ p384::ecdsa::VerifyingKey::from_sec1_bytes(bytes)?,
+ )),
+ crate::KEYTYPE_ECDSA_SHA2_NISTP521 => Ok(Self::P521(
+ local_p521::VerifyingKey::from_sec1_bytes(bytes)?,
+ )),
+ _ => Err(Error::UnsupportedKeyType {
+ key_type_string: String::from_utf8(algorithm.to_vec())
+ .unwrap_or_else(|_| format!("{algorithm:?}")),
+ key_type_raw: algorithm.to_vec(),
+ }),
+ }
+ }
+
+ /// Returns the SEC1-encoded public curve point.
+ pub fn to_sec1_bytes(&self) -> Vec {
+ match self {
+ Self::P256(key) => key.to_encoded_point(false).as_bytes().to_vec(),
+ Self::P384(key) => key.to_encoded_point(false).as_bytes().to_vec(),
+ Self::P521(key) => key.to_encoded_point(false).as_bytes().to_vec(),
+ }
+ }
+
+ /// Verifies message against signature `(r, s)` using the associated digest algorithm.
+ pub fn verify(&self, msg: &[u8], r: &[u8], s: &[u8]) -> Result<(), Error> {
+ use ecdsa::signature::Verifier;
+ match self {
+ Self::P256(key) => {
+ key.verify(msg, &signature_from_scalar_bytes::(r, s)?)
+ }
+ Self::P384(key) => {
+ key.verify(msg, &signature_from_scalar_bytes::(r, s)?)
+ }
+ Self::P521(key) => local_p521::verify(
+ key,
+ msg,
+ &signature_from_scalar_bytes::(r, s)?,
+ ),
+ }
+ .map_err(Error::from)
+ }
+}
+
+impl std::fmt::Debug for PublicKey {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match *self {
+ Self::P256(_) => write!(f, "P256"),
+ Self::P384(_) => write!(f, "P384"),
+ Self::P521(_) => write!(f, "P521"),
+ }
+ }
+}
+
+/// An ECC private key.
+#[derive(Clone, Eq, PartialEq)]
+pub enum PrivateKey {
+ P256(p256::ecdsa::SigningKey),
+ P384(p384::ecdsa::SigningKey),
+ P521(local_p521::SigningKey),
+}
+
+impl PrivateKey {
+ /// Creates a `PrivateKey` with algorithm name and scalar.
+ pub fn new_from_secret_scalar(algorithm: &[u8], scalar: &[u8]) -> Result {
+ match algorithm {
+ crate::KEYTYPE_ECDSA_SHA2_NISTP256 => {
+ Ok(Self::P256(p256::ecdsa::SigningKey::from_slice(scalar)?))
+ }
+ crate::KEYTYPE_ECDSA_SHA2_NISTP384 => {
+ Ok(Self::P384(p384::ecdsa::SigningKey::from_slice(scalar)?))
+ }
+ crate::KEYTYPE_ECDSA_SHA2_NISTP521 => {
+ Ok(Self::P521(local_p521::SigningKey::from_slice(scalar)?))
+ }
+ _ => Err(Error::UnsupportedKeyType {
+ key_type_string: String::from_utf8(algorithm.to_vec())
+ .unwrap_or_else(|_| format!("{algorithm:?}")),
+ key_type_raw: algorithm.to_vec(),
+ }),
+ }
+ }
+
+ /// Returns the elliptic curve domain parameter identifiers defined in RFC 5656 section 6.1.
+ pub fn ident(&self) -> &'static str {
+ match self {
+ Self::P256(_) => CURVE_NISTP256,
+ Self::P384(_) => CURVE_NISTP384,
+ Self::P521(_) => CURVE_NISTP521,
+ }
+ }
+
+ /// Returns the ECC public key algorithm name defined in RFC 5656 section 6.2, in the form of
+ /// `"ecdsa-sha2-[identifier]"`.
+ pub fn algorithm(&self) -> &'static str {
+ match self {
+ Self::P256(_) => crate::ECDSA_SHA2_NISTP256,
+ Self::P384(_) => crate::ECDSA_SHA2_NISTP384,
+ Self::P521(_) => crate::ECDSA_SHA2_NISTP521,
+ }
+ }
+
+ /// Returns the public key.
+ pub fn to_public_key(&self) -> PublicKey {
+ match self {
+ Self::P256(key) => PublicKey::P256(*key.verifying_key()),
+ Self::P384(key) => PublicKey::P384(*key.verifying_key()),
+ Self::P521(key) => PublicKey::P521(*key.verifying_key()),
+ }
+ }
+
+ /// Returns the secret scalar in bytes.
+ pub fn to_secret_bytes(&self) -> Vec {
+ match self {
+ Self::P256(key) => key.to_bytes().to_vec(),
+ Self::P384(key) => key.to_bytes().to_vec(),
+ Self::P521(key) => key.to_bytes().to_vec(),
+ }
+ }
+
+ /// Sign the message with associated digest algorithm.
+ pub fn try_sign(&self, msg: &[u8]) -> Result<(Vec, Vec), Error> {
+ use ecdsa::signature::RandomizedSigner;
+ Ok(match self {
+ Self::P256(key) => {
+ signature_to_scalar_bytes(key.try_sign_with_rng(&mut safe_rng(), msg)?)
+ }
+ Self::P384(key) => {
+ signature_to_scalar_bytes(key.try_sign_with_rng(&mut safe_rng(), msg)?)
+ }
+ Self::P521(key) => {
+ signature_to_scalar_bytes(local_p521::try_sign_with_rng(key, &mut safe_rng(), msg)?)
+ }
+ })
+ }
+}
+
+impl std::fmt::Debug for PrivateKey {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match *self {
+ Self::P256(_) => write!(f, "P256 {{ (hidden) }}"),
+ Self::P384(_) => write!(f, "P384 {{ (hidden) }}"),
+ Self::P521(_) => write!(f, "P521 {{ (hidden) }}"),
+ }
+ }
+}
+
+fn try_field_bytes_from_mpint(b: &[u8]) -> Option>
+where
+ C: Curve + CurveArithmetic,
+{
+ use typenum::Unsigned;
+ let size = FieldBytesSize::::to_usize();
+ assert!(size > 0);
+ #[allow(clippy::indexing_slicing)] // Length checked
+ if b.len() == size + 1 && b[0] == 0 {
+ Some(FieldBytes::::clone_from_slice(&b[1..]))
+ } else if b.len() == size {
+ Some(FieldBytes::::clone_from_slice(b))
+ } else if b.len() < size {
+ let mut fb: FieldBytes = Default::default();
+ fb.as_mut_slice()[size - b.len()..].clone_from_slice(b);
+ Some(fb)
+ } else {
+ None
+ }
+}
+
+fn signature_from_scalar_bytes(r: &[u8], s: &[u8]) -> Result, Error>
+where
+ C: Curve + CurveArithmetic + elliptic_curve::PrimeCurve,
+ ecdsa::SignatureSize: elliptic_curve::generic_array::ArrayLength,
+{
+ Ok(ecdsa::Signature::::from_scalars(
+ try_field_bytes_from_mpint::(r).ok_or(Error::InvalidSignature)?,
+ try_field_bytes_from_mpint::(s).ok_or(Error::InvalidSignature)?,
+ )?)
+}
+
+fn signature_to_scalar_bytes(sig: ecdsa::Signature) -> (Vec, Vec)
+where
+ C: Curve + CurveArithmetic + elliptic_curve::PrimeCurve,
+ ecdsa::SignatureSize: elliptic_curve::generic_array::ArrayLength,
+{
+ let (r, s) = sig.split_bytes();
+ (r.to_vec(), s.to_vec())
+}
diff --git a/russh-keys/src/encoding.rs b/russh-keys/src/encoding.rs
index 0f64f724..005196d3 100644
--- a/russh-keys/src/encoding.rs
+++ b/russh-keys/src/encoding.rs
@@ -41,6 +41,20 @@ pub trait Encoding {
fn extend_list>(&mut self, list: I);
/// Push an SSH-encoded empty list.
fn write_empty_list(&mut self);
+ /// Push an SSH-encoded value.
+ fn extend_ssh(&mut self, v: &T) {
+ v.write_ssh(self)
+ }
+ /// Push a nested SSH-encoded value.
+ fn extend_wrapped(&mut self, write: F)
+ where
+ F: FnOnce(&mut Self);
+}
+
+/// Trait for writing value in SSH-encoded format.
+pub trait SshWrite {
+ /// Write the value.
+ fn write_ssh(&self, encoder: &mut E);
}
/// Encoding length of the given mpint.
@@ -109,6 +123,20 @@ impl Encoding for Vec {
fn write_empty_list(&mut self) {
self.extend([0, 0, 0, 0]);
}
+
+ fn extend_wrapped(&mut self, write: F)
+ where
+ F: FnOnce(&mut Self),
+ {
+ let len_offset = self.len();
+ #[allow(clippy::unwrap_used)] // writing into Vec<> can't panic
+ self.write_u32::(0).unwrap();
+ let data_offset = self.len();
+ write(self);
+ let data_len = self.len() - data_offset;
+ #[allow(clippy::indexing_slicing)] // length is known
+ BigEndian::write_u32(&mut self[len_offset..], data_len as u32);
+ }
}
impl Encoding for CryptoVec {
@@ -163,6 +191,19 @@ impl Encoding for CryptoVec {
fn write_empty_list(&mut self) {
self.extend(&[0, 0, 0, 0]);
}
+
+ fn extend_wrapped(&mut self, write: F)
+ where
+ F: FnOnce(&mut Self),
+ {
+ let len_offset = self.len();
+ self.push_u32_be(0);
+ let data_offset = self.len();
+ write(self);
+ let data_len = self.len() - data_offset;
+ #[allow(clippy::indexing_slicing)] // length is known
+ BigEndian::write_u32(&mut self[len_offset..], data_len as u32);
+ }
}
/// A cursor-like trait to read SSH-encoded things.
@@ -244,4 +285,30 @@ impl<'a> Position<'a> {
Err(Error::IndexOutOfBounds)
}
}
+
+ pub fn read_ssh>(&mut self) -> Result {
+ T::read_ssh(self)
+ }
+}
+
+/// Trait for reading value in SSH-encoded format.
+pub trait SshRead<'a>: Sized + 'a {
+ /// Read the value from a position.
+ fn read_ssh(pos: &mut Position<'a>) -> Result;
+}
+
+impl<'a> ssh_encoding::Reader for Position<'a> {
+ fn read<'o>(&mut self, out: &'o mut [u8]) -> ssh_encoding::Result<&'o [u8]> {
+ out.copy_from_slice(
+ self.s
+ .get(self.position..(self.position + out.len()))
+ .ok_or(ssh_encoding::Error::Length)?,
+ );
+ self.position += out.len();
+ Ok(out)
+ }
+
+ fn remaining_len(&self) -> usize {
+ self.s.len() - self.position
+ }
}
diff --git a/russh-keys/src/format/mod.rs b/russh-keys/src/format/mod.rs
index 1463a85d..b120723c 100644
--- a/russh-keys/src/format/mod.rs
+++ b/russh-keys/src/format/mod.rs
@@ -1,21 +1,18 @@
use std::io::Write;
-#[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 super::is_base64_char;
use crate::{key, Error};
pub mod openssh;
+
+#[cfg(feature = "legacy-ed25519-pkcs8-parser")]
+mod pkcs8_legacy;
+
pub use self::openssh::*;
-#[cfg(feature = "openssl")]
pub mod pkcs5;
-#[cfg(feature = "openssl")]
pub use self::pkcs5::*;
pub mod pkcs8;
@@ -33,10 +30,8 @@ pub enum Encryption {
#[derive(Clone, Debug)]
enum Format {
- #[cfg(feature = "openssl")]
Rsa,
Openssh,
- #[cfg(feature = "openssl")]
Pkcs5Encrypted(Encryption),
Pkcs8Encrypted,
Pkcs8,
@@ -57,35 +52,22 @@ pub fn decode_secret_key(secret: &str, password: Option<&str>) -> Result = HEXLOWER_PERMISSIVE
- .decode(l.split_at(AES_128_CBC.len()).1.as_bytes())?;
- if iv_.len() != 16 {
- return Err(Error::CouldNotReadKey);
- }
- let mut iv = [0; 16];
- iv.clone_from_slice(&iv_);
- format = Some(Format::Pkcs5Encrypted(Encryption::Aes128Cbc(iv)))
+ let iv_: Vec =
+ HEXLOWER_PERMISSIVE.decode(l.split_at(AES_128_CBC.len()).1.as_bytes())?;
+ if iv_.len() != 16 {
+ return Err(Error::CouldNotReadKey);
}
+ 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(not(feature = "openssl"))]
- {
- return Err(Error::UnsupportedKeyType {
- key_type_string: "rsa".to_owned(),
- key_type_raw: "rsa".as_bytes().to_vec(),
- });
- }
- #[cfg(feature = "openssl")]
- {
- started = true;
- format = Some(Format::Rsa);
- }
+ started = true;
+ format = Some(Format::Rsa);
} else if l == "-----BEGIN ENCRYPTED PRIVATE KEY-----" {
started = true;
format = Some(Format::Pkcs8Encrypted);
@@ -100,19 +82,28 @@ pub fn decode_secret_key(secret: &str, password: Option<&str>) -> Result 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()))
+ let result = self::pkcs8::decode_pkcs8(&secret, password.map(|x| x.as_bytes()));
+ #[cfg(feature = "legacy-ed25519-pkcs8-parser")]
+ {
+ if result.is_err() {
+ let legacy_result =
+ pkcs8_legacy::decode_pkcs8(&secret, password.map(|x| x.as_bytes()));
+ if let Ok(key) = legacy_result {
+ return Ok(key);
+ }
+ }
+ }
+ result
}
None => Err(Error::CouldNotReadKey),
}
}
pub fn encode_pkcs8_pem(key: &key::KeyPair, mut w: W) -> Result<(), Error> {
- let x = self::pkcs8::encode_pkcs8(key);
+ 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")?;
@@ -132,10 +123,9 @@ pub fn encode_pkcs8_pem_encrypted(
Ok(())
}
-#[cfg(feature = "openssl")]
fn decode_rsa(secret: &[u8]) -> Result {
Ok(key::KeyPair::RSA {
- key: Rsa::private_key_from_der(secret)?,
+ key: crate::backend::RsaPrivate::new_from_der(secret)?,
hash: key::SignatureHash::SHA2_256,
})
}
diff --git a/russh-keys/src/format/openssh.rs b/russh-keys/src/format/openssh.rs
index 44821fb8..d0f2fdc6 100644
--- a/russh-keys/src/format/openssh.rs
+++ b/russh-keys/src/format/openssh.rs
@@ -1,169 +1,121 @@
use std::convert::TryFrom;
-use aes::cipher::block_padding::NoPadding;
-use aes::cipher::{BlockDecryptMut, KeyIvInit, StreamCipher};
-use bcrypt_pbkdf;
-use ctr::Ctr64BE;
-#[cfg(feature = "openssl")]
-use openssl::bn::BigNum;
+use ssh_key::private::{
+ EcdsaKeypair, Ed25519Keypair, KeypairData, PrivateKey, RsaKeypair, RsaPrivateKey,
+};
+use ssh_key::public::{Ed25519PublicKey, KeyData, RsaPublicKey};
+use ssh_key::{Algorithm, HashAlg};
-use crate::encoding::Reader;
-use crate::{key, Error, KEYTYPE_ED25519, KEYTYPE_RSA};
+use crate::key::{KeyPair, PublicKey, SignatureHash};
+use crate::{ec, protocol, Error};
/// 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 matches!(secret.get(0..15), Some(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()?;
+pub fn decode_openssh(secret: &[u8], password: Option<&str>) -> Result {
+ let pk = PrivateKey::from_bytes(secret)?;
+ KeyPair::try_from(&match password {
+ Some(password) => pk.decrypt(password)?,
+ None => pk,
+ })
+}
- // Read all public keys
- for _ in 0..nkeys {
- position.read_string()?;
- }
+impl TryFrom<&PrivateKey> for KeyPair {
+ type Error = Error;
- // 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()?;
- #[allow(clippy::never_loop)]
- for _ in 0..nkeys {
- // TODO check: never really loops beyond the first key
- 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()?;
- if Some(pubkey) != seckey.get(32..) {
+ fn try_from(pk: &PrivateKey) -> Result {
+ match pk.key_data() {
+ KeypairData::Ed25519(Ed25519Keypair { public, private }) => {
+ let key = ed25519_dalek::SigningKey::from(private.as_ref());
+ let public_key = ed25519_dalek::VerifyingKey::from_bytes(public.as_ref())?;
+ if public_key != key.verifying_key() {
return Err(Error::KeyIsCorrupt);
}
- let secret = ed25519_dalek::SigningKey::try_from(
- seckey.get(..32).ok_or(Error::KeyIsCorrupt)?,
- )?;
- 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()?;
- return Ok(key::KeyPair::RSA {
- key,
- hash: key::SignatureHash::SHA2_512,
- });
+ Ok(KeyPair::Ed25519(key))
+ }
+ KeypairData::Rsa(keypair) => {
+ KeyPair::new_rsa_with_hash(&keypair.into(), None, SignatureHash::SHA2_512)
+ }
+ KeypairData::Ecdsa(keypair) => {
+ let key_type = match keypair {
+ EcdsaKeypair::NistP256 { .. } => crate::KEYTYPE_ECDSA_SHA2_NISTP256,
+ EcdsaKeypair::NistP384 { .. } => crate::KEYTYPE_ECDSA_SHA2_NISTP384,
+ EcdsaKeypair::NistP521 { .. } => crate::KEYTYPE_ECDSA_SHA2_NISTP521,
+ };
+ let key =
+ ec::PrivateKey::new_from_secret_scalar(key_type, keypair.private_key_bytes())?;
+ let public_key =
+ ec::PublicKey::from_sec1_bytes(key_type, keypair.public_key_bytes())?;
+ if public_key != key.to_public_key() {
+ return Err(Error::KeyIsCorrupt);
}
- } else {
- return Err(Error::UnsupportedKeyType {
- key_type_string: String::from_utf8(key_type.to_vec())
- .unwrap_or_else(|_| format!("{key_type:?}")),
- key_type_raw: key_type.to_vec(),
- });
+ Ok(KeyPair::EC { key })
}
+ KeypairData::Encrypted(_) => Err(Error::KeyIsEncrypted),
+ _ => Err(Error::UnsupportedKeyType {
+ key_type_string: pk.algorithm().as_str().into(),
+ key_type_raw: pk.algorithm().as_str().as_bytes().into(),
+ }),
+ }
+ }
+}
+
+impl<'a> From<&'a RsaKeypair> for protocol::RsaPrivateKey<'a> {
+ fn from(key: &'a RsaKeypair) -> Self {
+ let RsaPublicKey { e, n } = &key.public;
+ let RsaPrivateKey { d, iqmp, p, q } = &key.private;
+ Self {
+ public_key: protocol::RsaPublicKey {
+ public_exponent: e.as_bytes().into(),
+ modulus: n.as_bytes().into(),
+ },
+ private_exponent: d.as_bytes().into(),
+ prime1: p.as_bytes().into(),
+ prime2: q.as_bytes().into(),
+ coefficient: iqmp.as_bytes().into(),
+ comment: b"".as_slice().into(),
}
- Err(Error::CouldNotReadKey)
- } else {
- Err(Error::CouldNotReadKey)
}
}
-use aes::*;
+impl TryFrom<&KeyData> for PublicKey {
+ type Error = Error;
-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)
+ fn try_from(key_data: &KeyData) -> Result {
+ match key_data {
+ KeyData::Ed25519(Ed25519PublicKey(public)) => Ok(PublicKey::Ed25519(
+ ed25519_dalek::VerifyingKey::from_bytes(public)?,
+ )),
+ KeyData::Rsa(ref public) => PublicKey::new_rsa_with_hash(
+ &public.into(),
+ match key_data.algorithm() {
+ Algorithm::Rsa { hash } => match hash {
+ Some(HashAlg::Sha256) => SignatureHash::SHA2_256,
+ Some(HashAlg::Sha512) => SignatureHash::SHA2_512,
+ _ => SignatureHash::SHA1,
+ },
+ _ => return Err(Error::KeyIsCorrupt),
+ },
+ ),
+ KeyData::Ecdsa(public) => Ok(PublicKey::EC {
+ key: ec::PublicKey::from_sec1_bytes(
+ key_data.algorithm().as_str().as_bytes(),
+ public.as_sec1_bytes(),
+ )?,
+ }),
+ _ => Err(Error::UnsupportedKeyType {
+ key_type_string: key_data.algorithm().as_str().into(),
+ key_type_raw: key_data.algorithm().as_str().as_bytes().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),
- };
- match kdfname {
- b"bcrypt" => {
- let mut kdfopts = kdfoptions.reader(0);
- let salt = kdfopts.read_string()?;
- let rounds = kdfopts.read_u32()?;
- #[allow(clippy::unwrap_used)] // parameters are static
- #[allow(clippy::indexing_slicing)] // output length is static
- match bcrypt_pbkdf::bcrypt_pbkdf(password, salt, rounds, &mut key[..n]) {
- Err(bcrypt_pbkdf::Error::InvalidParamLen) => return Err(Error::KeyIsEncrypted),
- e => e.unwrap(),
- }
- }
- _kdfname => {
- return Err(Error::CouldNotReadKey);
- }
- };
- let (key, iv) = key.split_at(n - 16);
+ }
+}
- let mut dec = secret_key.to_vec();
- dec.resize(dec.len() + 32, 0u8);
- match ciphername {
- b"aes128-cbc" => {
- #[allow(clippy::unwrap_used)] // parameters are static
- let cipher = cbc::Decryptor::::new_from_slices(key, iv).unwrap();
- let n = cipher.decrypt_padded_mut::(&mut dec)?.len();
- dec.truncate(n)
- }
- b"aes256-cbc" => {
- #[allow(clippy::unwrap_used)] // parameters are static
- let cipher = cbc::Decryptor::::new_from_slices(key, iv).unwrap();
- let n = cipher.decrypt_padded_mut::(&mut dec)?.len();
- dec.truncate(n)
- }
- b"aes128-ctr" => {
- #[allow(clippy::unwrap_used)] // parameters are static
- let mut cipher = Ctr64BE::::new_from_slices(key, iv).unwrap();
- cipher.apply_keystream(&mut dec);
- dec.truncate(secret_key.len())
- }
- b"aes256-ctr" => {
- #[allow(clippy::unwrap_used)] // parameters are static
- let mut cipher = Ctr64BE::::new_from_slices(key, iv).unwrap();
- cipher.apply_keystream(&mut dec);
- dec.truncate(secret_key.len())
- }
- _ => {}
+impl<'a> From<&'a RsaPublicKey> for protocol::RsaPublicKey<'a> {
+ fn from(key: &'a RsaPublicKey) -> Self {
+ let RsaPublicKey { e, n } = key;
+ Self {
+ public_exponent: e.as_bytes().into(),
+ modulus: n.as_bytes().into(),
}
- Ok(dec)
- } else {
- Err(Error::KeyIsEncrypted)
}
}
diff --git a/russh-keys/src/format/pkcs5.rs b/russh-keys/src/format/pkcs5.rs
index 0e5a2a5e..b1b4c266 100644
--- a/russh-keys/src/format/pkcs5.rs
+++ b/russh-keys/src/format/pkcs5.rs
@@ -3,9 +3,8 @@ use aes::*;
use super::Encryption;
use crate::{key, Error};
-/// Decode a secret key in the PKCS#5 format, possible deciphering it
+/// Decode a secret key in the PKCS#5 format, possibly deciphering it
/// using the supplied password.
-#[cfg(feature = "openssl")]
pub fn decode_pkcs5(
secret: &[u8],
password: Option<&str>,
@@ -25,8 +24,7 @@ pub fn decode_pkcs5(
#[allow(clippy::unwrap_used)] // AES parameters are static
let c = cbc::Decryptor::::new_from_slices(&md5.0, &iv[..]).unwrap();
let mut dec = secret.to_vec();
- c.decrypt_padded_mut::(&mut dec)?;
- dec
+ c.decrypt_padded_mut::(&mut dec)?.to_vec()
}
Encryption::Aes256Cbc(_) => unimplemented!(),
};
diff --git a/russh-keys/src/format/pkcs8.rs b/russh-keys/src/format/pkcs8.rs
index a9cbb96d..ceb8a313 100644
--- a/russh-keys/src/format/pkcs8.rs
+++ b/russh-keys/src/format/pkcs8.rs
@@ -1,259 +1,134 @@
-use std::borrow::Cow;
+use std::convert::{TryFrom, TryInto};
-use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
-use bit_vec::BitVec;
-use block_padding::{NoPadding, Pkcs7};
-#[cfg(feature = "openssl")]
-use openssl::pkey::Private;
-#[cfg(feature = "openssl")]
-use openssl::rsa::Rsa;
-#[cfg(test)]
-use rand_core::OsRng;
-use std::convert::TryFrom;
-use yasna::BERReaderSeq;
-use {std, yasna};
+use pkcs8::{EncodePrivateKey, PrivateKeyInfo, SecretDocument};
-use super::Encryption;
-#[cfg(feature = "openssl")]
use crate::key::SignatureHash;
-use crate::{key, Error};
-
-const PBES2: &[u64] = &[1, 2, 840, 113549, 1, 5, 13];
-const PBKDF2: &[u64] = &[1, 2, 840, 113549, 1, 5, 12];
-const HMAC_SHA256: &[u64] = &[1, 2, 840, 113549, 2, 9];
-const AES256CBC: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 1, 42];
-const ED25519: &[u64] = &[1, 3, 101, 112];
-#[cfg(feature = "openssl")]
-const RSA: &[u64] = &[1, 2, 840, 113549, 1, 1, 1];
+use crate::{ec, key, protocol, Error};
/// 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)))
- }
- })?;
- // Ciphertext
- let ciphertext = reader.next().read_bytes()?;
- Ok(parameters.map(|p| p.decrypt(pass, &ciphertext)))
- })
- })???)
+ let doc = SecretDocument::try_from(ciphertext)?;
+ let doc = if let Some(password) = password {
+ doc.decode_msg::()?
+ .decrypt(password)?
} else {
- Cow::Borrowed(ciphertext)
+ doc
};
- 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))
+ key::KeyPair::try_from(doc.decode_msg::()?)
+}
+
+impl<'a> TryFrom> for key::KeyPair {
+ type Error = Error;
+
+ fn try_from(pki: PrivateKeyInfo<'a>) -> Result {
+ match pki.algorithm.oid {
+ ed25519_dalek::pkcs8::ALGORITHM_OID => Ok(key::KeyPair::Ed25519(
+ ed25519_dalek::pkcs8::KeypairBytes::try_from(pki)?
+ .secret_key
+ .into(),
+ )),
+ pkcs1::ALGORITHM_OID => {
+ let sk = &pkcs1::RsaPrivateKey::try_from(pki.private_key)?;
+ key::KeyPair::new_rsa_with_hash(
+ &sk.into(),
+ Some(&sk.into()),
+ SignatureHash::SHA2_256,
+ )
}
- })
- })?
+ sec1::ALGORITHM_OID => Ok(key::KeyPair::EC {
+ key: pki.try_into()?,
+ }),
+ oid => Err(Error::UnknownAlgorithm(oid)),
+ }
+ }
}
-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))))
- })
+impl<'a> From<&pkcs1::RsaPrivateKey<'a>> for protocol::RsaPrivateKey<'a> {
+ fn from(sk: &pkcs1::RsaPrivateKey<'a>) -> Self {
+ Self {
+ public_key: protocol::RsaPublicKey {
+ public_exponent: sk.public_exponent.as_bytes().into(),
+ modulus: sk.modulus.as_bytes().into(),
+ },
+ private_exponent: sk.private_exponent.as_bytes().into(),
+ prime1: sk.prime1.as_bytes().into(),
+ prime2: sk.prime2.as_bytes().into(),
+ coefficient: sk.coefficient.as_bytes().into(),
+ comment: b"".as_slice().into(),
+ }
+ }
}
-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 }))
- })
+impl<'a> From<&pkcs1::RsaPrivateKey<'a>> for key::RsaCrtExtra<'a> {
+ fn from(sk: &pkcs1::RsaPrivateKey<'a>) -> Self {
+ Self {
+ dp: sk.exponent1.as_bytes().into(),
+ dq: sk.exponent2.as_bytes().into(),
+ }
+ }
}
-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)))
-}
+// Note: It's infeasible to implement `EncodePrivateKey` because that is bound to `pkcs8::Result`.
+impl TryFrom<&key::RsaPrivate> for SecretDocument {
+ type Error = Error;
-fn write_key_v1(writer: &mut yasna::DERWriterSeq, secret: &ed25519_dalek::SigningKey) {
- let public = ed25519_dalek::VerifyingKey::from(secret);
- 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.to_bytes().as_slice(), public.as_bytes().as_slice()]
- .concat()
- .as_slice(),
- )
- });
- writer.next().write_bytes(&seed);
- writer
- .next()
- .write_tagged(yasna::Tag::context(1), |writer| {
- writer.write_bitvec(&BitVec::from_bytes(public.as_bytes()))
- })
-}
+ fn try_from(key: &key::RsaPrivate) -> Result {
+ use der::Encode;
+ use pkcs1::UintRef;
-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 ed25519_dalek::SigningKey;
- let secret = {
- let s = yasna::parse_der(&reader.next().read_bytes()?, |reader| reader.read_bytes())?;
+ let sk = protocol::RsaPrivateKey::try_from(key)?;
+ let extra = key::RsaCrtExtra::try_from(key)?;
- s.get(..ed25519_dalek::SECRET_KEY_LENGTH)
- .ok_or(Error::KeyIsCorrupt)
- .and_then(|s| SigningKey::try_from(s).map_err(|_| Error::CouldNotReadKey))?
+ let rsa_private_key = pkcs1::RsaPrivateKey {
+ modulus: UintRef::new(&sk.public_key.modulus)?,
+ public_exponent: UintRef::new(&sk.public_key.public_exponent)?,
+ private_exponent: UintRef::new(&sk.private_exponent)?,
+ prime1: UintRef::new(&sk.prime1)?,
+ prime2: UintRef::new(&sk.prime2)?,
+ exponent1: UintRef::new(&extra.dp)?,
+ exponent2: UintRef::new(&extra.dq)?,
+ coefficient: UintRef::new(&sk.coefficient)?,
+ other_prime_infos: None,
};
- // Consume the public key
- reader
- .next()
- .read_tagged(yasna::Tag::context(1), |reader| reader.read_bitvec())?;
- Ok(key::KeyPair::Ed25519(secret))
- } else {
- Err(Error::CouldNotReadKey)
+ let pki = PrivateKeyInfo {
+ algorithm: spki::AlgorithmIdentifier {
+ oid: pkcs1::ALGORITHM_OID,
+ parameters: Some(der::asn1::Null.into()),
+ },
+ private_key: &rsa_private_key.to_der()?,
+ public_key: None,
+ };
+ Ok(Self::try_from(pki)?)
}
}
-#[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| {
- #[allow(clippy::unwrap_used)] // key is known to be private
- 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);
-}
+impl TryFrom> for ec::PrivateKey {
+ type Error = Error;
-#[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));
- }
- 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)
+ fn try_from(pki: PrivateKeyInfo<'_>) -> Result {
+ use pkcs8::AssociatedOid;
+ match pki.algorithm.parameters_oid()? {
+ p256::NistP256::OID => Ok(ec::PrivateKey::P256(pki.try_into()?)),
+ p384::NistP384::OID => Ok(ec::PrivateKey::P384(pki.try_into()?)),
+ p521::NistP521::OID => Ok(ec::PrivateKey::P521(pki.try_into()?)),
+ oid => Err(Error::UnknownAlgorithm(oid)),
+ }
}
}
-#[cfg(not(feature = "openssl"))]
-fn read_key_v0(_: &mut BERReaderSeq) -> Result {
- Err(Error::CouldNotReadKey)
+impl EncodePrivateKey for ec::PrivateKey {
+ fn to_pkcs8_der(&self) -> pkcs8::Result {
+ match self {
+ ec::PrivateKey::P256(key) => key.to_pkcs8_der(),
+ ec::PrivateKey::P384(key) => key.to_pkcs8_der(),
+ ec::PrivateKey::P521(key) => key.to_pkcs8_der(),
+ }
+ }
}
#[test]
fn test_read_write_pkcs8() {
- let secret = ed25519_dalek::SigningKey::generate(&mut OsRng {});
+ let secret = ed25519_dalek::SigningKey::generate(&mut key::safe_rng());
assert_eq!(
secret.verifying_key().as_bytes(),
ed25519_dalek::VerifyingKey::from(&secret).as_bytes()
@@ -264,176 +139,43 @@ fn test_read_write_pkcs8() {
let key = decode_pkcs8(&ciphertext, Some(password)).unwrap();
match key {
key::KeyPair::Ed25519 { .. } => println!("Ed25519"),
- #[cfg(feature = "openssl")]
+ key::KeyPair::EC { .. } => println!("EC"),
key::KeyPair::RSA { .. } => println!("RSA"),
}
}
-use aes::*;
-use yasna::models::ObjectIdentifier;
-
/// Encode a password-protected PKCS#8-encoded private key.
pub fn encode_pkcs8_encrypted(
pass: &[u8],
rounds: u32,
key: &key::KeyPair,
) -> Result, Error> {
+ let pvi_bytes = encode_pkcs8(key)?;
+ let pvi = PrivateKeyInfo::try_from(pvi_bytes.as_slice())?;
+
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));
-
- #[allow(clippy::unwrap_used)] // parameters are static
- let c = cbc::Encryptor::::new_from_slices(&dkey, &iv).unwrap();
- let n = plaintext.len();
- let encrypted = c.encrypt_padded_mut::(&mut plaintext, n)?;
- 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(encrypted)
- })
- }))
+ let doc = pvi.encrypt_with_params(
+ pkcs5::pbes2::Parameters::pbkdf2_sha256_aes256cbc(rounds, &salt, &iv)
+ .map_err(|_| Error::InvalidParameters)?,
+ pass,
+ )?;
+ Ok(doc.as_bytes().to_vec())
}
/// 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 pair) => write_key_v1(writer, pair),
- #[cfg(feature = "openssl")]
- key::KeyPair::RSA { ref key, .. } => write_key_v0(writer, key),
- })
- })
-}
-
-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(())
+pub fn encode_pkcs8(key: &key::KeyPair) -> Result, Error> {
+ let v = match *key {
+ key::KeyPair::Ed25519(ref pair) => pair.to_pkcs8_der()?,
+ key::KeyPair::RSA { ref key, .. } => SecretDocument::try_from(key)?,
+ key::KeyPair::EC { ref key, .. } => key.to_pkcs8_der()?,
}
-}
-
-#[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) => {
- #[allow(clippy::unwrap_used)] // parameters are static
- let c = cbc::Decryptor::::new_from_slices(key, iv).unwrap();
- let mut dec = ciphertext.to_vec();
- Ok(c.decrypt_padded_mut::(&mut dec)?.into())
- }
- Encryption::Aes256Cbc(ref iv) => {
- #[allow(clippy::unwrap_used)] // parameters are static
- let c = cbc::Decryptor::::new_from_slices(key, iv).unwrap();
- let mut dec = ciphertext.to_vec();
- Ok(c.decrypt_padded_mut::(&mut dec)?.into())
- }
- }
- }
-}
-
-enum KeyDerivation {
- Pbkdf2 { salt: Vec, rounds: u64 },
+ .as_bytes()
+ .to_vec();
+ Ok(v)
}
diff --git a/russh-keys/src/format/pkcs8_legacy.rs b/russh-keys/src/format/pkcs8_legacy.rs
new file mode 100644
index 00000000..5553ccee
--- /dev/null
+++ b/russh-keys/src/format/pkcs8_legacy.rs
@@ -0,0 +1,212 @@
+use std::borrow::Cow;
+use std::convert::TryFrom;
+
+use aes::cipher::{BlockDecryptMut, KeyIvInit};
+use aes::*;
+use block_padding::Pkcs7;
+use yasna::BERReaderSeq;
+
+use super::Encryption;
+use crate::{key, Error};
+
+const PBES2: &[u64] = &[1, 2, 840, 113549, 1, 5, 13];
+const ED25519: &[u64] = &[1, 3, 101, 112];
+const PBKDF2: &[u64] = &[1, 2, 840, 113549, 1, 5, 12];
+const AES256CBC: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 1, 42];
+const HMAC_SHA256: &[u64] = &[1, 2, 840, 113549, 2, 9];
+
+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::InvalidParameters))
+ }
+ })?;
+ // 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(Err(Error::CouldNotReadKey))
+ } else if version == 1 {
+ Ok(read_key_v1(reader))
+ } else {
+ Ok(Err(Error::CouldNotReadKey))
+ }
+ })
+ })?
+}
+
+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 ed25519_dalek::SigningKey;
+ let secret = {
+ let s = yasna::parse_der(&reader.next().read_bytes()?, |reader| reader.read_bytes())?;
+
+ s.get(..ed25519_dalek::SECRET_KEY_LENGTH)
+ .ok_or(Error::KeyIsCorrupt)
+ .and_then(|s| SigningKey::try_from(s).map_err(|_| Error::CouldNotReadKey))?
+ };
+ // Consume the public key
+ reader
+ .next()
+ .read_tagged(yasna::Tag::context(1), |reader| reader.read_bitvec())?;
+ Ok(key::KeyPair::Ed25519(secret))
+ } else {
+ Err(Error::CouldNotReadKey)
+ }
+}
+
+#[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,
+ }
+ }
+}
+
+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 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) => {
+ #[allow(clippy::unwrap_used)] // parameters are static
+ let c = cbc::Decryptor::::new_from_slices(key, iv).unwrap();
+ let mut dec = ciphertext.to_vec();
+ Ok(c.decrypt_padded_mut::(&mut dec)?.into())
+ }
+ Encryption::Aes256Cbc(ref iv) => {
+ #[allow(clippy::unwrap_used)] // parameters are static
+ let c = cbc::Decryptor::::new_from_slices(key, iv).unwrap();
+ let mut dec = ciphertext.to_vec();
+ Ok(c.decrypt_padded_mut::(&mut dec)?.into())
+ }
+ }
+ }
+}
+
+enum KeyDerivation {
+ Pbkdf2 { salt: Vec, rounds: u64 },
+}
+
+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(())
+ }
+}
+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::InvalidParameters))
+ }
+ })?;
+ // 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::InvalidParameters))
+ }
+ })?;
+ 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::InvalidParameters))
+ }
+ })?;
+ 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)))
+}
diff --git a/russh-keys/src/key.rs b/russh-keys/src/key.rs
index 98ebda9f..f187d163 100644
--- a/russh-keys/src/key.rs
+++ b/russh-keys/src/key.rs
@@ -12,17 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
+use std::borrow::Cow;
+use std::convert::{TryFrom, TryInto};
+
+pub use backend::{RsaPrivate, RsaPublic};
use ed25519_dalek::{Signer, Verifier};
-#[cfg(feature = "openssl")]
-use openssl::pkey::{Private, Public};
use rand_core::OsRng;
use russh_cryptovec::CryptoVec;
use serde::{Deserialize, Serialize};
-use std::convert::TryFrom;
use crate::encoding::{Encoding, Reader};
pub use crate::signature::*;
-use crate::Error;
+use crate::{backend, ec, protocol, Error};
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
/// Name of a public key algorithm.
@@ -34,6 +35,12 @@ impl AsRef for Name {
}
}
+/// The name of the ecdsa-sha2-nistp256 algorithm for SSH.
+pub const ECDSA_SHA2_NISTP256: Name = Name("ecdsa-sha2-nistp256");
+/// The name of the ecdsa-sha2-nistp384 algorithm for SSH.
+pub const ECDSA_SHA2_NISTP384: Name = Name("ecdsa-sha2-nistp384");
+/// The name of the ecdsa-sha2-nistp521 algorithm for SSH.
+pub const ECDSA_SHA2_NISTP521: Name = Name("ecdsa-sha2-nistp521");
/// 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.
@@ -45,10 +52,21 @@ pub const NONE: Name = Name("none");
pub const SSH_RSA: Name = Name("ssh-rsa");
+pub static ALL_KEY_TYPES: &[&Name] = &[
+ &NONE,
+ &SSH_RSA,
+ &RSA_SHA2_256,
+ &RSA_SHA2_512,
+ &ECDSA_SHA2_NISTP256,
+ &ECDSA_SHA2_NISTP384,
+ &ECDSA_SHA2_NISTP521,
+];
+
impl Name {
/// Base name of the private key file for a key name.
pub fn identity_file(&self) -> &'static str {
match *self {
+ ECDSA_SHA2_NISTP256 | ECDSA_SHA2_NISTP384 | ECDSA_SHA2_NISTP521 => "id_ecdsa",
ED25519 => "id_ed25519",
RSA_SHA2_512 => "id_rsa",
RSA_SHA2_256 => "id_rsa",
@@ -57,6 +75,17 @@ impl Name {
}
}
+impl TryFrom<&str> for Name {
+ type Error = ();
+ fn try_from(s: &str) -> Result {
+ ALL_KEY_TYPES
+ .iter()
+ .find(|x| x.0 == s)
+ .map(|x| **x)
+ .ok_or(())
+ }
+}
+
#[doc(hidden)]
pub trait Verify {
fn verify_client_auth(&self, buffer: &[u8], sig: &[u8]) -> bool;
@@ -84,23 +113,12 @@ impl SignatureHash {
}
}
- #[cfg(feature = "openssl")]
- fn 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(),
- }
- }
-
pub fn from_rsa_hostkey_algo(algo: &[u8]) -> Option {
- if algo == b"rsa-sha2-256" {
- Some(Self::SHA2_256)
- } else if algo == b"rsa-sha2-512" {
- Some(Self::SHA2_512)
- } else {
- Some(Self::SHA1)
+ match algo {
+ b"rsa-sha2-256" => Some(Self::SHA2_256),
+ b"rsa-sha2-512" => Some(Self::SHA2_512),
+ b"ssh-rsa" => Some(Self::SHA1),
+ _ => None,
}
}
}
@@ -111,108 +129,61 @@ pub enum PublicKey {
#[doc(hidden)]
Ed25519(ed25519_dalek::VerifyingKey),
#[doc(hidden)]
- #[cfg(feature = "openssl")]
RSA {
- key: OpenSSLPKey,
+ key: backend::RsaPublic,
hash: SignatureHash,
},
+ #[doc(hidden)]
+ EC { key: ec::PublicKey },
}
impl PartialEq for PublicKey {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
- #[cfg(feature = "openssl")]
(Self::RSA { key: a, .. }, Self::RSA { key: b, .. }) => a == b,
(Self::Ed25519(a), Self::Ed25519(b)) => a == b,
- #[cfg(feature = "openssl")]
+ (Self::EC { key: a }, Self::EC { key: b }) => a == b,
_ => false,
}
}
}
-/// A public key from OpenSSL.
-#[cfg(feature = "openssl")]
-#[derive(Clone)]
-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" {
- return Err(Error::CouldNotReadKey);
- }
- let Ok(key_bytes) = <&[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>::try_from(key_bytes) else {
- return Err(Error::CouldNotReadKey);
- };
- ed25519_dalek::VerifyingKey::from_bytes(key_bytes)
- .map(PublicKey::Ed25519)
- .map_err(Error::from)
+ use ssh_encoding::Decode;
+ let key_data = &ssh_key::public::KeyData::decode(&mut pubkey.reader(0))?;
+ let key_algo = key_data.algorithm();
+ let key_algo = key_algo.as_str().as_bytes();
+ if key_algo == b"ssh-rsa" {
+ if algo != SSH_RSA.as_ref().as_bytes()
+ && algo != RSA_SHA2_256.as_ref().as_bytes()
+ && algo != RSA_SHA2_512.as_ref().as_bytes()
+ {
+ return Err(Error::KeyIsCorrupt);
}
- b"ssh-rsa" | b"rsa-sha2-256" | b"rsa-sha2-512" if cfg!(feature = "openssl") => {
- #[cfg(feature = "openssl")]
- {
- use log::debug;
- 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);
- }
- 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: SignatureHash::from_rsa_hostkey_algo(algo)
- .unwrap_or(SignatureHash::SHA1),
- })
- }
- #[cfg(not(feature = "openssl"))]
- {
- unreachable!()
- }
- }
- _ => Err(Error::CouldNotReadKey),
+ } else if key_algo != algo {
+ return Err(Error::KeyIsCorrupt);
}
+ Self::try_from(key_data)
+ }
+
+ pub fn new_rsa_with_hash(
+ pk: &protocol::RsaPublicKey<'_>,
+ hash: SignatureHash,
+ ) -> Result {
+ Ok(PublicKey::RSA {
+ key: RsaPublic::try_from(pk)?,
+ hash,
+ })
}
/// 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,
+ PublicKey::EC { ref key } => key.algorithm(),
}
}
@@ -226,17 +197,8 @@ impl PublicKey {
let sig = ed25519_dalek::Signature::from_bytes(&sig);
public.verify(buffer, &sig).is_ok()
}
-
- #[cfg(feature = "openssl")]
- PublicKey::RSA { ref key, ref hash } => {
- use openssl::sign::*;
- let verify = || {
- let mut verifier = Verifier::new(hash.message_digest(), &key.0)?;
- verifier.update(buffer)?;
- verifier.verify(sig)
- };
- verify().unwrap_or(false)
- }
+ PublicKey::RSA { ref key, ref hash } => key.verify_detached(hash, buffer, sig),
+ PublicKey::EC { ref key, .. } => ec_verify(key, buffer, sig).is_ok(),
}
}
@@ -250,21 +212,11 @@ impl PublicKey {
data_encoding::BASE64_NOPAD.encode(&hasher.finalize())
}
- #[cfg(feature = "openssl")]
- pub fn set_algorithm(&mut self, algorithm: &[u8]) {
+ pub fn set_algorithm(&mut self, algorithm: SignatureHash) {
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
- }
+ *hash = algorithm;
}
}
-
- #[cfg(not(feature = "openssl"))]
- pub fn set_algorithm(&mut self, _: &[u8]) {}
}
impl Verify for PublicKey {
@@ -280,11 +232,13 @@ impl Verify for PublicKey {
#[allow(clippy::large_enum_variant)]
pub enum KeyPair {
Ed25519(ed25519_dalek::SigningKey),
- #[cfg(feature = "openssl")]
RSA {
- key: openssl::rsa::Rsa,
+ key: backend::RsaPrivate,
hash: SignatureHash,
},
+ EC {
+ key: ec::PrivateKey,
+ },
}
impl Clone for KeyPair {
@@ -294,11 +248,11 @@ impl Clone for KeyPair {
Self::Ed25519(kp) => {
Self::Ed25519(ed25519_dalek::SigningKey::from_bytes(&kp.to_bytes()))
}
- #[cfg(feature = "openssl")]
Self::RSA { key, hash } => Self::RSA {
key: key.clone(),
hash: *hash,
},
+ Self::EC { key } => Self::EC { key: key.clone() },
}
}
}
@@ -311,8 +265,8 @@ impl std::fmt::Debug for KeyPair {
"Ed25519 {{ public: {:?}, secret: (hidden) }}",
key.verifying_key().as_bytes()
),
- #[cfg(feature = "openssl")]
KeyPair::RSA { .. } => write!(f, "RSA {{ (hidden) }}"),
+ KeyPair::EC { .. } => write!(f, "EC {{ (hidden) }}"),
}
}
}
@@ -324,20 +278,28 @@ impl<'b> crate::encoding::Bytes for &'b KeyPair {
}
impl KeyPair {
+ pub fn new_rsa_with_hash(
+ sk: &protocol::RsaPrivateKey<'_>,
+ extra: Option<&RsaCrtExtra<'_>>,
+ hash: SignatureHash,
+ ) -> Result {
+ Ok(KeyPair::RSA {
+ key: RsaPrivate::new(sk, extra)?,
+ hash,
+ })
+ }
+
/// Copy the public key of this algorithm.
pub fn clone_public_key(&self) -> Result {
Ok(match self {
KeyPair::Ed25519(ref key) => PublicKey::Ed25519(key.verifying_key()),
- #[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()?, key.e().to_owned()?)?;
- PublicKey::RSA {
- key: OpenSSLPKey(PKey::from_rsa(key)?),
- hash: *hash,
- }
- }
+ KeyPair::RSA { ref key, ref hash } => PublicKey::RSA {
+ key: key.try_into()?,
+ hash: *hash,
+ },
+ KeyPair::EC { ref key } => PublicKey::EC {
+ key: key.to_public_key(),
+ },
})
}
@@ -345,12 +307,12 @@ impl KeyPair {
pub fn name(&self) -> &'static str {
match *self {
KeyPair::Ed25519(_) => ED25519.0,
- #[cfg(feature = "openssl")]
KeyPair::RSA { ref hash, .. } => hash.name().0,
+ KeyPair::EC { ref key } => key.algorithm(),
}
}
- /// Generate a key pair.
+ /// Generate a ED25519 key pair.
pub fn generate_ed25519() -> Option {
let keypair = ed25519_dalek::SigningKey::generate(&mut OsRng {});
assert_eq!(
@@ -360,9 +322,9 @@ impl KeyPair {
Some(KeyPair::Ed25519(keypair))
}
- #[cfg(feature = "openssl")]
+ /// Generate a RSA key pair.
pub fn generate_rsa(bits: usize, hash: SignatureHash) -> Option {
- let key = openssl::rsa::Rsa::generate(bits as u32).ok()?;
+ let key = RsaPrivate::generate(bits).ok()?;
Some(KeyPair::RSA { key, hash })
}
@@ -373,11 +335,14 @@ impl KeyPair {
KeyPair::Ed25519(ref secret) => Ok(Signature::Ed25519(SignatureBytes(
secret.sign(to_sign).to_bytes(),
))),
- #[cfg(feature = "openssl")]
KeyPair::RSA { ref key, ref hash } => Ok(Signature::RSA {
- bytes: rsa_signature(hash, key, to_sign)?,
+ bytes: key.sign(hash, to_sign)?,
hash: *hash,
}),
+ KeyPair::EC { ref key } => Ok(Signature::ECDSA {
+ algorithm: key.algorithm(),
+ signature: ec_signature(key, to_sign)?,
+ }),
}
}
@@ -398,15 +363,21 @@ impl KeyPair {
buffer.extend_ssh_string(ED25519.0.as_bytes());
buffer.extend_ssh_string(signature.to_bytes().as_slice());
}
- #[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 signature = key.sign(hash, 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);
}
+ KeyPair::EC { ref key } => {
+ let algorithm = key.algorithm().as_bytes();
+ let signature = ec_signature(key, to_sign.as_ref())?;
+ buffer.push_u32_be((algorithm.len() + signature.len() + 8) as u32);
+ buffer.extend_ssh_string(algorithm);
+ buffer.extend_ssh_string(&signature);
+ }
}
Ok(())
}
@@ -423,89 +394,88 @@ impl KeyPair {
buffer.extend_ssh_string(ED25519.0.as_bytes());
buffer.extend_ssh_string(signature.to_bytes().as_slice());
}
- #[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 signature = key.sign(hash, 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);
}
+ KeyPair::EC { ref key } => {
+ let signature = ec_signature(key, buffer)?;
+ let algorithm = key.algorithm().as_bytes();
+ buffer.push_u32_be((algorithm.len() + signature.len() + 8) as u32);
+ buffer.extend_ssh_string(algorithm);
+ buffer.extend_ssh_string(&signature);
+ }
}
Ok(())
}
/// Create a copy of an RSA key with a specified hash algorithm.
- #[cfg(feature = "openssl")]
pub fn with_signature_hash(&self, hash: SignatureHash) -> Option {
match self {
KeyPair::Ed25519(_) => None,
- #[cfg(feature = "openssl")]
KeyPair::RSA { key, .. } => Some(KeyPair::RSA {
key: key.clone(),
hash,
}),
+ KeyPair::EC { .. } => None,
}
}
}
-#[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().ok_or(Error::KeyIsCorrupt)?.to_owned()?,
- key.q().ok_or(Error::KeyIsCorrupt)?.to_owned()?,
- key.dmp1().ok_or(Error::KeyIsCorrupt)?.to_owned()?,
- key.dmq1().ok_or(Error::KeyIsCorrupt)?.to_owned()?,
- key.iqmp().ok_or(Error::KeyIsCorrupt)?.to_owned()?,
- )?)?;
- let mut signer = Signer::new(hash.message_digest(), &pkey)?;
- signer.update(b)?;
- Ok(signer.sign_to_vec()?)
+/// Extra CRT parameters for RSA private key.
+pub struct RsaCrtExtra<'a> {
+ /// `d mod (p-1)`.
+ pub dp: Cow<'a, [u8]>,
+ /// `d mod (q-1)`.
+ pub dq: Cow<'a, [u8]>,
}
-/// Parse a public key from a byte slice.
-pub fn parse_public_key(
- p: &[u8],
- #[cfg(feature = "openssl")] prefer_hash: Option,
-) -> Result {
- let mut pos = p.reader(0);
- let t = pos.read_string()?;
- if t == b"ssh-ed25519" {
- if let Ok(pubkey) = pos.read_string() {
- let Ok(pubkey) = <&[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>::try_from(pubkey) else {
- return Err(Error::CouldNotReadKey);
- };
- let p = ed25519_dalek::VerifyingKey::from_bytes(pubkey).map_err(Error::from)?;
- return Ok(PublicKey::Ed25519(p));
- }
+impl Drop for RsaCrtExtra<'_> {
+ fn drop(&mut self) {
+ zeroize_cow(&mut self.dp);
+ zeroize_cow(&mut self.dq);
}
- 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: prefer_hash.unwrap_or(SignatureHash::SHA2_256),
- });
- }
+}
+
+fn ec_signature(key: &ec::PrivateKey, b: &[u8]) -> Result, Error> {
+ let (r, s) = key.try_sign(b)?;
+ let mut buf = Vec::new();
+ buf.extend_ssh_mpint(&r);
+ buf.extend_ssh_mpint(&s);
+ Ok(buf)
+}
+
+fn ec_verify(key: &ec::PublicKey, b: &[u8], sig: &[u8]) -> Result<(), Error> {
+ let mut reader = sig.reader(0);
+ key.verify(b, reader.read_mpint()?, reader.read_mpint()?)
+}
+
+/// Parse a public key from a byte slice.
+pub fn parse_public_key(p: &[u8], prefer_hash: Option) -> Result {
+ use ssh_encoding::Decode;
+ let mut key = PublicKey::try_from(&ssh_key::public::KeyData::decode(&mut p.reader(0))?)?;
+ key.set_algorithm(prefer_hash.unwrap_or(SignatureHash::SHA2_256));
+ Ok(key)
+}
+
+/// Obtain a cryptographic-safe random number generator.
+pub fn safe_rng() -> impl rand::CryptoRng + rand::RngCore {
+ rand::thread_rng()
+}
+
+/// Zeroize `Cow` if value is owned.
+pub(crate) fn zeroize_cow(v: &mut Cow)
+where
+ T: ToOwned + ?Sized,
+ ::Owned: zeroize::Zeroize,
+{
+ use zeroize::Zeroize;
+ match v {
+ Cow::Owned(v) => v.zeroize(),
+ Cow::Borrowed(_) => (),
}
- Err(Error::CouldNotReadKey)
}
diff --git a/russh-keys/src/lib.rs b/russh-keys/src/lib.rs
index 09287aa0..25e65ee9 100644
--- a/russh-keys/src/lib.rs
+++ b/russh-keys/src/lib.rs
@@ -10,11 +10,11 @@
//! 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).
+//! The following example 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 russh_keys::*;
@@ -30,7 +30,7 @@
//!
//! 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";
//!
-//! #[cfg(all(unix, feature = "openssl"))]
+//! #[cfg(unix)]
//! fn main() {
//! env_logger::try_init().unwrap_or(());
//! let dir = tempdir::TempDir::new("russh").unwrap();
@@ -58,7 +58,7 @@
//! }).unwrap()
//! }
//!
-//! #[cfg(any(not(unix), not(feature = "openssl")))]
+//! #[cfg(not(unix))]
//! fn main() {}
//!
//! ```
@@ -66,23 +66,35 @@
use std::borrow::Cow;
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
-use std::path::Path;
+use std::path::{Path, PathBuf};
use aes::cipher::block_padding::UnpadError;
use aes::cipher::inout::PadError;
use byteorder::{BigEndian, WriteBytesExt};
use data_encoding::BASE64_MIME;
+use hmac::{Hmac, Mac};
use log::debug;
+use sha1::Sha1;
+use ssh_key::Certificate;
use thiserror::Error;
+pub mod ec;
pub mod encoding;
pub mod key;
+pub mod protocol;
pub mod signature;
mod format;
pub use format::*;
-/// A module to write SSH agent.
+#[cfg(feature = "openssl")]
+#[path = "backend_openssl.rs"]
+mod backend;
+#[cfg(not(feature = "openssl"))]
+#[path = "backend_rust.rs"]
+mod backend;
+
+/// OpenSSH agent protocol implementation
pub mod agent;
#[derive(Debug, Error)]
@@ -99,6 +111,9 @@ pub enum Error {
/// The type of the key is unsupported
#[error("Invalid Ed25519 key data")]
Ed25519KeyError(#[from] ed25519_dalek::SignatureError),
+ /// The type of the key is unsupported
+ #[error("Invalid ECDSA key data")]
+ EcdsaKeyError(#[from] p256::elliptic_curve::Error),
/// The key is encrypted (should supply a password?)
#[error("The key is encrypted")]
KeyIsEncrypted,
@@ -112,14 +127,18 @@ pub enum Error {
#[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),
+ #[error("Unknown key algorithm: {0}")]
+ UnknownAlgorithm(::pkcs8::ObjectIdentifier),
/// Index out of bounds
#[error("Index out of bounds")]
IndexOutOfBounds,
/// Unknown signature type
#[error("Unknown signature type: {}", sig_type)]
UnknownSignatureType { sig_type: String },
+ #[error("Invalid signature")]
+ InvalidSignature,
+ #[error("Invalid parameters")]
+ InvalidParameters,
/// Agent protocol error
#[error("Agent protocol error")]
AgentProtocolError,
@@ -132,6 +151,10 @@ pub enum Error {
#[error(transparent)]
Openssl(#[from] openssl::error::ErrorStack),
+ #[cfg(not(feature = "openssl"))]
+ #[error("Rsa: {0}")]
+ Rsa(#[from] rsa::Error),
+
#[error(transparent)]
Pad(#[from] PadError),
@@ -140,8 +163,22 @@ pub enum Error {
#[error("Base64 decoding error: {0}")]
Decode(#[from] data_encoding::DecodeError),
- #[error("ASN1 decoding error: {0}")]
- ASN1(yasna::ASN1Error),
+ #[error("Der: {0}")]
+ Der(#[from] der::Error),
+ #[error("Spki: {0}")]
+ Spki(#[from] spki::Error),
+ #[error("Pkcs1: {0}")]
+ Pkcs1(#[from] pkcs1::Error),
+ #[error("Pkcs8: {0}")]
+ Pkcs8(#[from] ::pkcs8::Error),
+ #[error("Sec1: {0}")]
+ Sec1(#[from] sec1::Error),
+
+ #[error("SshKey: {0}")]
+ SshKey(#[from] ssh_key::Error),
+ #[error("SshEncoding: {0}")]
+ SshEncoding(#[from] ssh_encoding::Error),
+
#[error("Environment variable `{0}` not found")]
EnvVar(&'static str),
#[error(
@@ -149,18 +186,32 @@ pub enum Error {
points to a nonexistent file or directory."
)]
BadAuthSock,
+
+ #[error("ASN1 decoding error: {0}")]
+ #[cfg(feature = "legacy-ed25519-pkcs8-parser")]
+ LegacyASN1(::yasna::ASN1Error),
+
+ #[cfg(windows)]
+ #[error("Pageant: {0}")]
+ Pageant(#[from] pageant::Error),
}
+#[cfg(feature = "legacy-ed25519-pkcs8-parser")]
impl From for Error {
fn from(e: yasna::ASN1Error) -> Error {
- Error::ASN1(e)
+ Error::LegacyASN1(e)
}
}
-const KEYTYPE_ED25519: &[u8] = b"ssh-ed25519";
-const KEYTYPE_RSA: &[u8] = b"ssh-rsa";
+const KEYTYPE_ECDSA_SHA2_NISTP256: &[u8] = ECDSA_SHA2_NISTP256.as_bytes();
+const KEYTYPE_ECDSA_SHA2_NISTP384: &[u8] = ECDSA_SHA2_NISTP384.as_bytes();
+const KEYTYPE_ECDSA_SHA2_NISTP521: &[u8] = ECDSA_SHA2_NISTP521.as_bytes();
+
+const ECDSA_SHA2_NISTP256: &str = "ecdsa-sha2-nistp256";
+const ECDSA_SHA2_NISTP384: &str = "ecdsa-sha2-nistp384";
+const ECDSA_SHA2_NISTP521: &str = "ecdsa-sha2-nistp521";
-/// Load a public key from a file. Ed25519 and RSA keys are supported.
+/// Load a public key from a file. Ed25519, EC-DSA and RSA keys are supported.
///
/// ```
/// russh_keys::load_public_key("../files/id_ed25519.pub").unwrap();
@@ -187,11 +238,7 @@ pub fn load_public_key>(path: P) -> Result
/// ```
pub fn parse_public_key_base64(key: &str) -> Result {
let base = BASE64_MIME.decode(key.as_bytes())?;
- key::parse_public_key(
- &base,
- #[cfg(feature = "openssl")]
- None,
- )
+ key::parse_public_key(&base, None)
}
pub trait PublicKeyBase64 {
@@ -219,17 +266,16 @@ impl PublicKeyBase64 for key::PublicKey {
.unwrap();
s.extend_from_slice(publickey.as_bytes());
}
- #[cfg(feature = "openssl")]
key::PublicKey::RSA { ref key, .. } => {
use encoding::Encoding;
let name = b"ssh-rsa";
#[allow(clippy::unwrap_used)] // Vec<>.write_all can't fail
s.write_u32::(name.len() as u32).unwrap();
s.extend_from_slice(name);
- #[allow(clippy::unwrap_used)] // TODO check
- s.extend_ssh_mpint(&key.0.rsa().unwrap().e().to_vec());
- #[allow(clippy::unwrap_used)] // TODO check
- s.extend_ssh_mpint(&key.0.rsa().unwrap().n().to_vec());
+ s.extend_ssh(&protocol::RsaPublicKey::from(key));
+ }
+ key::PublicKey::EC { ref key } => {
+ write_ec_public_key(&mut s, key);
}
}
s
@@ -250,17 +296,29 @@ impl PublicKeyBase64 for key::KeyPair {
s.write_u32::(public.len() as u32).unwrap();
s.extend_from_slice(public.as_slice());
}
- #[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.extend_ssh(&protocol::RsaPublicKey::from(key));
+ }
+ key::KeyPair::EC { ref key } => {
+ write_ec_public_key(&mut s, &key.to_public_key());
}
}
s
}
}
+fn write_ec_public_key(buf: &mut Vec, key: &ec::PublicKey) {
+ let algorithm = key.algorithm().as_bytes();
+ let ident = key.ident().as_bytes();
+ let q = key.to_sec1_bytes();
+
+ use encoding::Encoding;
+ buf.extend_ssh_string(algorithm);
+ buf.extend_ssh_string(ident);
+ buf.extend_ssh_string(&q);
+}
+
/// Write a public key onto the provided `Write`, encoded in base-64.
pub fn write_public_key_base64(
mut w: W,
@@ -282,15 +340,29 @@ pub fn load_secret_key>(
decode_secret_key(&secret, password)
}
+/// Load a openssh certificate
+pub fn load_openssh_certificate>(cert_: P) -> Result {
+ let mut cert_file = std::fs::File::open(cert_)?;
+ let mut cert = String::new();
+ cert_file.read_to_string(&mut cert)?;
+
+ Certificate::from_openssh(&cert)
+}
+
fn is_base64_char(c: char) -> bool {
- ('a'..='z').contains(&c)
- || ('A'..='Z').contains(&c)
- || ('0'..='9').contains(&c)
+ c.is_ascii_lowercase()
+ || c.is_ascii_uppercase()
+ || c.is_ascii_digit()
|| c == '/'
|| c == '+'
|| c == '='
}
+/// Record a host's public key into the user's known_hosts file.
+pub fn learn_known_hosts(host: &str, port: u16, pubkey: &key::PublicKey) -> Result<(), Error> {
+ learn_known_hosts_path(host, port, pubkey, known_hosts_path()?)
+}
+
/// Record a host's public key into a nonstandard location.
pub fn learn_known_hosts_path>(
host: &str,
@@ -331,17 +403,21 @@ pub fn learn_known_hosts_path>(
Ok(())
}
-/// Check that a server key matches the one recorded in file `path`.
-pub fn check_known_hosts_path>(
+/// Get the server key that matches the one recorded in the user's known_hosts file.
+pub fn known_host_keys(host: &str, port: u16) -> Result, Error> {
+ known_host_keys_path(host, port, known_hosts_path()?)
+}
+
+/// Get the server key that matches the one recorded in `path`.
+pub fn known_host_keys_path>(
host: &str,
port: u16,
- pubkey: &key::PublicKey,
path: P,
-) -> Result {
+) -> Result, Error> {
let mut f = if let Ok(f) = File::open(path) {
BufReader::new(f)
} else {
- return Ok(false);
+ return Ok(vec![]);
};
let mut buffer = String::new();
@@ -352,6 +428,7 @@ pub fn check_known_hosts_path