diff options
-rw-r--r-- | libssh2-sys/lib.rs | 31 | ||||
-rw-r--r-- | src/lib.rs | 2 | ||||
-rw-r--r-- | src/session.rs | 157 | ||||
-rw-r--r-- | systest/build.rs | 8 | ||||
-rw-r--r-- | tests/all/session.rs | 73 | ||||
-rwxr-xr-x | tests/run_integration_tests.sh | 1 |
6 files changed, 267 insertions, 5 deletions
diff --git a/libssh2-sys/lib.rs b/libssh2-sys/lib.rs index 5ce1dc2..8157dab 100644 --- a/libssh2-sys/lib.rs +++ b/libssh2-sys/lib.rs @@ -266,6 +266,30 @@ pub type LIBSSH2_PASSWD_CHANGEREQ_FUNC = extern "C" fn( abstrakt: *mut *mut c_void, ); +pub type LIBSSH2_USERAUTH_KBDINT_RESPONSE_FUNC = extern "C" fn( + username: *const c_char, + username_len: c_int, + instruction: *const c_char, + instruction_len: c_int, + num_prompts: c_int, + prompts: *const LIBSSH2_USERAUTH_KBDINT_PROMPT, + responses: *mut LIBSSH2_USERAUTH_KBDINT_RESPONSE, + abstrakt: *mut *mut c_void, +); + +#[repr(C)] +pub struct LIBSSH2_USERAUTH_KBDINT_PROMPT { + pub text: *mut c_char, + pub length: c_uint, + pub echo: c_uchar, +} + +#[repr(C)] +pub struct LIBSSH2_USERAUTH_KBDINT_RESPONSE { + pub text: *mut c_char, + pub length: c_uint, +} + #[cfg(unix)] pub type libssh2_socket_t = c_int; #[cfg(all(windows, target_arch = "x86"))] @@ -287,6 +311,7 @@ extern "C" { realloc: Option<LIBSSH2_REALLOC_FUNC>, abstrakt: *mut c_void, ) -> *mut LIBSSH2_SESSION; + pub fn libssh2_session_abstract(session: *mut LIBSSH2_SESSION) -> *mut *mut c_void; pub fn libssh2_session_free(sess: *mut LIBSSH2_SESSION) -> c_int; pub fn libssh2_session_banner_get(sess: *mut LIBSSH2_SESSION) -> *const c_char; pub fn libssh2_session_banner_set(sess: *mut LIBSSH2_SESSION, banner: *const c_char) -> c_int; @@ -499,6 +524,12 @@ extern "C" { password_len: c_uint, password_change_cb: Option<LIBSSH2_PASSWD_CHANGEREQ_FUNC>, ) -> c_int; + pub fn libssh2_userauth_keyboard_interactive_ex( + session: *mut LIBSSH2_SESSION, + username: *const c_char, + username_len: c_uint, + callback: Option<LIBSSH2_USERAUTH_KBDINT_RESPONSE_FUNC>, + ) -> c_int; // knownhost pub fn libssh2_knownhost_free(hosts: *mut LIBSSH2_KNOWNHOSTS); @@ -142,7 +142,7 @@ pub use error::Error; pub use knownhosts::{Host, Hosts, KnownHosts}; pub use listener::Listener; use session::SessionInner; -pub use session::{ScpFileStat, Session}; +pub use session::{KeyboardInteractivePrompt, Prompt, ScpFileStat, Session}; pub use sftp::{File, FileStat, FileType, OpenType}; pub use sftp::{OpenFlags, RenameFlags, Sftp}; pub use DisconnectCode::{AuthCancelledByUser, TooManyConnections}; diff --git a/src/session.rs b/src/session.rs index bce81ad..30be94d 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,6 +1,7 @@ #[cfg(unix)] use libc::size_t; -use libc::{self, c_int, c_long, c_uint, c_void}; +use libc::{self, c_char, c_int, c_long, c_uint, c_void}; +use std::borrow::Cow; use std::cell::{Ref, RefCell}; use std::ffi::CString; use std::mem; @@ -14,6 +15,54 @@ use util; use {raw, ByApplication, DisconnectCode, Error, HostKeyType}; use {Agent, Channel, HashType, KnownHosts, Listener, MethodType, Sftp}; +/// Called by libssh2 to respond to some number of challenges as part of +/// keyboard interactive authentication. +pub trait KeyboardInteractivePrompt { + /// `username` is the user name to be authenticated. It may not be the + /// same as the username passed to `Session::userauth_keyboard_interactive`, + /// and may be empty. + /// `instructions` is some informational text to be displayed to the user. + /// `prompts` is a series of prompts (or challenges) that must be responded + /// to. + /// The return value should be a Vec that holds one response for each prompt. + fn prompt<'a>( + &mut self, + username: &str, + instructions: &str, + prompts: &[Prompt<'a>], + ) -> Vec<String>; +} + +/// A prompt/challenge returned as part of keyboard-interactive authentication +#[derive(Debug)] +pub struct Prompt<'a> { + /// The label to show when prompting the user + pub text: Cow<'a, str>, + /// If true, the response that the user inputs should be displayed + /// as they type. If false then treat it as a password entry and + /// do not display what is typed in response to this prompt. + pub echo: bool, +} + +/// This is a little helper function that is perhaps slightly overkill for the +/// current needs. +/// It saves the current sess->abstract pointer and replaces it with a +/// different values for the duration of the call to the supplied lambda. +/// When the lambda returns, the original abstract value is restored +/// and the result of the lambda is returned. +unsafe fn with_abstract<R, F: FnOnce() -> R>( + sess: *mut raw::LIBSSH2_SESSION, + new_value: *mut c_void, + f: F, +) -> R { + let abstrakt = raw::libssh2_session_abstract(sess); + let old_value = *abstrakt; + *abstrakt = new_value; + let res = f(); + *abstrakt = old_value; + res +} + pub(crate) struct SessionInner { pub(crate) raw: *mut raw::LIBSSH2_SESSION, tcp: RefCell<Option<TcpStream>>, @@ -216,6 +265,112 @@ impl Session { }) } + /// Attempt keyboard interactive authentication. + /// + /// You must supply a callback function to + pub fn userauth_keyboard_interactive<P: KeyboardInteractivePrompt>( + &self, + username: &str, + prompter: &mut P, + ) -> Result<(), Error> { + // hold on to your hats, this is a bit involved. + // The keyboard interactive callback is a bit tricksy, and we want to wrap the + // raw C types with something a bit safer and more ergonomic. + // Since the interface is defined in terms of a simple function pointer, wrapping + // is a bit awkward. + // + // The session struct has an abstrakt pointer reserved for + // the user of the embedding application, and that pointer is passed to the + // prompt callback. We can use this to store a pointer to some state so that + // we can manage the conversion. + // + // The prompts and responses are defined to be UTF-8, but we use from_utf8_lossy + // to avoid panics in case the server isn't conformant for whatever reason. + extern "C" fn prompt<P: KeyboardInteractivePrompt>( + username: *const c_char, + username_len: c_int, + instruction: *const c_char, + instruction_len: c_int, + num_prompts: c_int, + prompts: *const raw::LIBSSH2_USERAUTH_KBDINT_PROMPT, + responses: *mut raw::LIBSSH2_USERAUTH_KBDINT_RESPONSE, + abstrakt: *mut *mut c_void, + ) { + let prompter = unsafe { &mut **(abstrakt as *mut *mut P) }; + + let username = + unsafe { slice::from_raw_parts(username as *const u8, username_len as usize) }; + let username = String::from_utf8_lossy(username); + + let instruction = unsafe { + slice::from_raw_parts(instruction as *const u8, instruction_len as usize) + }; + let instruction = String::from_utf8_lossy(instruction); + + let prompts = unsafe { slice::from_raw_parts(prompts, num_prompts as usize) }; + let responses = + unsafe { slice::from_raw_parts_mut(responses, num_prompts as usize) }; + + let prompts: Vec<Prompt> = prompts + .iter() + .map(|item| { + let data = unsafe { + slice::from_raw_parts(item.text as *const u8, item.length as usize) + }; + Prompt { + text: String::from_utf8_lossy(data), + echo: item.echo != 0, + } + }) + .collect(); + + // libssh2 wants to be able to free(3) the response strings, so allocate + // storage and copy the responses into appropriately owned memory. + // We can't simply call strdup(3) here because the rust string types + // are not NUL terminated. + fn strdup_string(s: &str) -> *mut c_char { + let len = s.len(); + let ptr = unsafe { libc::malloc(len + 1) as *mut c_char }; + if !ptr.is_null() { + unsafe { + ::std::ptr::copy_nonoverlapping( + s.as_bytes().as_ptr() as *const c_char, + ptr, + len, + ); + *ptr.offset(len as isize) = 0; + } + } + ptr + } + + for (i, response) in (*prompter) + .prompt(&username, &instruction, &prompts) + .into_iter() + .enumerate() + { + let ptr = strdup_string(&response); + if !ptr.is_null() { + responses[i].length = response.len() as c_uint; + } else { + responses[i].length = 0; + } + responses[i].text = ptr; + } + } + + unsafe { + with_abstract(self.inner.raw, prompter as *mut P as *mut c_void, || { + self.rc(raw::libssh2_userauth_keyboard_interactive_ex( + self.inner.raw, + username.as_ptr() as *const _, + username.len() as c_uint, + Some(prompt::<P>), + )) + }) + } + } + /// Attempt to perform SSH agent authentication. /// /// This is a helper method for attempting to authenticate the current diff --git a/systest/build.rs b/systest/build.rs index 7d5c281..2b3e66a 100644 --- a/systest/build.rs +++ b/systest/build.rs @@ -24,7 +24,11 @@ fn main() { s.to_string() } }) - .skip_type(|t| t.ends_with("FUNC")) - .skip_fn(|f| f == "libssh2_userauth_password_ex" || f == "libssh2_session_init_ex"); + .skip_type(|t| t.ends_with("FUNC") || t.contains("KBDINT")) + .skip_fn(|f| { + f == "libssh2_userauth_password_ex" + || f == "libssh2_session_init_ex" + || f == "libssh2_userauth_keyboard_interactive_ex" + }); cfg.generate("../libssh2-sys/lib.rs", "all.rs"); } diff --git a/tests/all/session.rs b/tests/all/session.rs index 2c636a2..5e45d53 100644 --- a/tests/all/session.rs +++ b/tests/all/session.rs @@ -4,7 +4,7 @@ use std::io::prelude::*; use std::path::Path; use tempdir::TempDir; -use ssh2::{HashType, MethodType, Session}; +use ssh2::{HashType, KeyboardInteractivePrompt, MethodType, Prompt, Session}; #[test] fn smoke() { @@ -48,6 +48,77 @@ fn smoke_handshake() { } #[test] +fn keyboard_interactive() { + let user = env::var("USER").unwrap(); + let socket = ::socket(); + let mut sess = Session::new().unwrap(); + sess.handshake(socket).unwrap(); + sess.host_key().unwrap(); + let methods = sess.auth_methods(&user).unwrap(); + assert!(methods.contains("keyboard-interactive"), "{}", methods); + assert!(!sess.authenticated()); + + // We don't know the correct response for whatever challenges + // will be returned to us, but that's ok; the purpose of this + // test is to check that we have some basically sane interaction + // with the library. + + struct Prompter { + some_data: usize, + } + + impl KeyboardInteractivePrompt for Prompter { + fn prompt<'a>( + &mut self, + username: &str, + instructions: &str, + prompts: &[Prompt<'a>], + ) -> Vec<String> { + // Sanity check that the pointer manipulation resolves and + // we read back our member data ok + assert_eq!(self.some_data, 42); + + eprintln!("username: {}", username); + eprintln!("instructions: {}", instructions); + eprintln!("prompts: {:?}", prompts); + + // Unfortunately, we can't make any assertions about username + // or instructions, as they can be empty (on my linux system) + // or may have arbitrary contents + // assert_eq!(username, env::var("USER").unwrap()); + // assert!(!instructions.is_empty()); + + // Hopefully this isn't too brittle an assertion + if prompts.len() == 1 { + assert_eq!(prompts.len(), 1); + // Might be "Password: " or "Password:" or other variations + assert!(prompts[0].text.contains("sword")); + assert_eq!(prompts[0].echo, false); + } else { + // maybe there's some PAM configuration that results + // in multiple prompts. We can't make any real assertions + // in this case, other than that there has to be at least + // one prompt. + assert!(!prompts.is_empty()); + } + + prompts.iter().map(|_| "bogus".to_string()).collect() + } + } + + let mut p = Prompter { some_data: 42 }; + + match sess.userauth_keyboard_interactive(&user, &mut p) { + Ok(_) => eprintln!("auth succeeded somehow(!)"), + Err(err) => eprintln!("auth failed as expected: {}", err), + }; + + // The only way this assertion will be false is if the person + // running these tests has "bogus" as their password + assert!(!sess.authenticated()); +} + +#[test] fn keepalive() { let sess = ::authed_session(); sess.set_keepalive(false, 10); diff --git a/tests/run_integration_tests.sh b/tests/run_integration_tests.sh index 784a734..a1f8b0b 100755 --- a/tests/run_integration_tests.sh +++ b/tests/run_integration_tests.sh @@ -46,6 +46,7 @@ UsePAM yes X11Forwarding yes PrintMotd yes PermitTunnel yes +KbdInteractiveAuthentication yes AllowTcpForwarding yes MaxStartups 500 # Relax modes when the repo is under eg: /var/tmp |