From c505918a86fb9ac8c19e47cd751a9db4e2d9efb2 Mon Sep 17 00:00:00 2001
From: Hans Nilsson
Date: Thu, 29 Oct 2015 19:51:04 +0100
Subject: ssh: pwdfun/4 and simple tests
Also solves OTP-13053
---
lib/ssh/src/ssh.erl | 4 +-
lib/ssh/src/ssh.hrl | 1 +
lib/ssh/src/ssh_auth.erl | 49 +++++++----
lib/ssh/test/ssh_options_SUITE.erl | 161 ++++++++++++++++++++++++++++++++++++-
4 files changed, 199 insertions(+), 16 deletions(-)
(limited to 'lib')
diff --git a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl
index 049018b21c..6f79b48091 100644
--- a/lib/ssh/src/ssh.erl
+++ b/lib/ssh/src/ssh.erl
@@ -462,7 +462,9 @@ handle_ssh_option({password, Value} = Opt) when is_list(Value) ->
Opt;
handle_ssh_option({user_passwords, Value} = Opt) when is_list(Value)->
Opt;
-handle_ssh_option({pwdfun, Value} = Opt) when is_function(Value) ->
+handle_ssh_option({pwdfun, Value} = Opt) when is_function(Value,2) ->
+ Opt;
+handle_ssh_option({pwdfun, Value} = Opt) when is_function(Value,4) ->
Opt;
handle_ssh_option({key_cb, Value} = Opt) when is_atom(Value) ->
Opt;
diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl
index fc9d60c500..4ad936f742 100644
--- a/lib/ssh/src/ssh.hrl
+++ b/lib/ssh/src/ssh.hrl
@@ -138,6 +138,7 @@
kb_tries_left = 0, % integer(), num tries left for "keyboard-interactive"
userauth_preference,
available_host_keys,
+ pwdfun_user_state,
authenticated = false
}).
diff --git a/lib/ssh/src/ssh_auth.erl b/lib/ssh/src/ssh_auth.erl
index 04749fcf8e..4272eb3c52 100644
--- a/lib/ssh/src/ssh_auth.erl
+++ b/lib/ssh/src/ssh_auth.erl
@@ -174,15 +174,15 @@ handle_userauth_request(#ssh_msg_userauth_request{user = User,
#ssh{opts = Opts,
userauth_supported_methods = Methods} = Ssh) ->
Password = unicode:characters_to_list(BinPwd),
- case check_password(User, Password, Opts) of
- true ->
+ case check_password(User, Password, Opts, Ssh) of
+ {true,Ssh1} ->
{authorized, User,
- ssh_transport:ssh_packet(#ssh_msg_userauth_success{}, Ssh)};
- false ->
+ ssh_transport:ssh_packet(#ssh_msg_userauth_success{}, Ssh1)};
+ {false,Ssh1} ->
{not_authorized, {User, {error,"Bad user or password"}},
ssh_transport:ssh_packet(#ssh_msg_userauth_failure{
authentications = Methods,
- partial_success = false}, Ssh)}
+ partial_success = false}, Ssh1)}
end;
handle_userauth_request(#ssh_msg_userauth_request{user = User,
@@ -335,16 +335,16 @@ handle_userauth_info_response(#ssh_msg_userauth_info_response{num_responses = 1,
kb_tries_left = KbTriesLeft,
user = User,
userauth_supported_methods = Methods} = Ssh) ->
- case check_password(User, unicode:characters_to_list(Password), Opts) of
- true ->
+ case check_password(User, unicode:characters_to_list(Password), Opts, Ssh) of
+ {true,Ssh1} ->
{authorized, User,
- ssh_transport:ssh_packet(#ssh_msg_userauth_success{}, Ssh)};
- false ->
+ ssh_transport:ssh_packet(#ssh_msg_userauth_success{}, Ssh1)};
+ {false,Ssh1} ->
{not_authorized, {User, {error,"Bad user or password"}},
ssh_transport:ssh_packet(#ssh_msg_userauth_failure{
authentications = Methods,
partial_success = false},
- Ssh#ssh{kb_tries_left = max(KbTriesLeft-1, 0)}
+ Ssh1#ssh{kb_tries_left = max(KbTriesLeft-1, 0)}
)}
end;
@@ -387,13 +387,34 @@ user_name(Opts) ->
{ok, User}
end.
-check_password(User, Password, Opts) ->
+check_password(User, Password, Opts, Ssh) ->
case proplists:get_value(pwdfun, Opts) of
undefined ->
Static = get_password_option(Opts, User),
- Password == Static;
- Cheker ->
- Cheker(User, Password)
+ {Password == Static, Ssh};
+
+ Checker when is_function(Checker,2) ->
+ {Checker(User, Password), Ssh};
+
+ Checker when is_function(Checker,4) ->
+ #ssh{pwdfun_user_state = PrivateState,
+ peer = {_,PeerAddr={_,_}}
+ } = Ssh,
+ case Checker(User, Password, PeerAddr, PrivateState) of
+ true ->
+ {true,Ssh};
+ false ->
+ {false,Ssh};
+ {true,NewState} ->
+ {true, Ssh#ssh{pwdfun_user_state=NewState}};
+ {false,NewState} ->
+ {false, Ssh#ssh{pwdfun_user_state=NewState}};
+ disconnect ->
+ throw(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_SERVICE_NOT_AVAILABLE,
+ description =
+ "Unable to connect using the available authentication methods",
+ language = ""})
+ end
end.
get_password_option(Opts, User) ->
diff --git a/lib/ssh/test/ssh_options_SUITE.erl b/lib/ssh/test/ssh_options_SUITE.erl
index cf15ca4253..6a201d401f 100644
--- a/lib/ssh/test/ssh_options_SUITE.erl
+++ b/lib/ssh/test/ssh_options_SUITE.erl
@@ -45,6 +45,9 @@
max_sessions_ssh_connect_sequential/1,
server_password_option/1,
server_userpassword_option/1,
+ server_pwdfun_option/1,
+ server_pwdfun_4_option/1,
+ server_pwdfun_4_option_repeat/1,
ssh_connect_arg4_timeout/1,
ssh_connect_negtimeout_parallel/1,
ssh_connect_negtimeout_sequential/1,
@@ -83,6 +86,9 @@ all() ->
connectfun_disconnectfun_client,
server_password_option,
server_userpassword_option,
+ server_pwdfun_option,
+ server_pwdfun_4_option,
+ server_pwdfun_4_option_repeat,
{group, dir_options},
ssh_connect_timeout,
ssh_connect_arg4_timeout,
@@ -188,7 +194,9 @@ init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(TestCase, Config) when TestCase == server_password_option;
- TestCase == server_userpassword_option ->
+ TestCase == server_userpassword_option;
+ TestCase == server_pwdfun_option;
+ TestCase == server_pwdfun_4_option ->
UserDir = filename:join(?config(priv_dir, Config), nopubkey),
ssh_test_lib:del_dirs(UserDir),
end_per_testcase(Config);
@@ -271,6 +279,157 @@ server_userpassword_option(Config) when is_list(Config) ->
{user_dir, UserDir}]),
ssh:stop_daemon(Pid).
+%%--------------------------------------------------------------------
+%%% validate to server that uses the 'pwdfun' option
+server_pwdfun_option(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
+ file:make_dir(UserDir),
+ SysDir = ?config(data_dir, Config),
+ CHKPWD = fun("foo",Pwd) -> Pwd=="bar";
+ (_,_) -> false
+ end,
+ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
+ {user_dir, PrivDir},
+ {pwdfun,CHKPWD}]),
+ ConnectionRef =
+ ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {password, "bar"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ssh:close(ConnectionRef),
+
+ Reason = "Unable to connect using the available authentication methods",
+
+ {error, Reason} =
+ ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {password, "morot"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ {error, Reason} =
+ ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "vego"},
+ {password, "foo"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ssh:stop_daemon(Pid).
+
+
+%%--------------------------------------------------------------------
+%%% validate to server that uses the 'pwdfun/4' option
+server_pwdfun_4_option(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
+ file:make_dir(UserDir),
+ SysDir = ?config(data_dir, Config),
+ PWDFUN = fun("foo",Pwd,{_,_},undefined) -> Pwd=="bar";
+ ("fie",Pwd,{_,_},undefined) -> {Pwd=="bar",new_state};
+ ("bandit",_,_,_) -> disconnect;
+ (_,_,_,_) -> false
+ end,
+ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
+ {user_dir, PrivDir},
+ {pwdfun,PWDFUN}]),
+ ConnectionRef1 =
+ ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {password, "bar"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ssh:close(ConnectionRef1),
+
+ ConnectionRef2 =
+ ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "fie"},
+ {password, "bar"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ssh:close(ConnectionRef2),
+
+ Reason = "Unable to connect using the available authentication methods",
+
+ {error, Reason} =
+ ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {password, "morot"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ {error, Reason} =
+ ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "fie"},
+ {password, "morot"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ {error, Reason} =
+ ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "vego"},
+ {password, "foo"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+
+ {error, Reason} =
+ ssh:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "bandit"},
+ {password, "pwd breaking"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ssh:stop_daemon(Pid).
+
+
+%%--------------------------------------------------------------------
+server_pwdfun_4_option_repeat(Config) ->
+ PrivDir = ?config(priv_dir, Config),
+ UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
+ file:make_dir(UserDir),
+ SysDir = ?config(data_dir, Config),
+ %% Test that the state works
+ Parent = self(),
+ PWDFUN = fun("foo",P="bar",_,S) -> Parent!{P,S},true;
+ (_,P,_,S=undefined) -> Parent!{P,S},{false,1};
+ (_,P,_,S) -> Parent!{P,S}, {false,S+1}
+ end,
+ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
+ {user_dir, PrivDir},
+ {auth_methods,"keyboard-interactive"},
+ {pwdfun,PWDFUN}]),
+
+ %% Try with passwords "incorrect", "Bad again" and finally "bar"
+ KIFFUN = fun(_,_,_) ->
+ K={k,self()},
+ case get(K) of
+ undefined ->
+ put(K,1),
+ ["incorrect"];
+ 2 ->
+ put(K,3),
+ ["bar"];
+ S->
+ put(K,S+1),
+ ["Bad again"]
+ end
+ end,
+
+ ConnectionRef2 =
+ ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {keyboard_interact_fun, KIFFUN},
+ {user_dir, UserDir}]),
+ ssh:close(ConnectionRef2),
+ ssh:stop_daemon(Pid),
+
+ lists:foreach(fun(Expect) ->
+ receive
+ Expect -> ok;
+ Other -> ct:fail("Expect: ~p~nReceived ~p",[Expect,Other])
+ after
+ 2000 -> ct:fail("Timeout expecting ~p",[Expect])
+ end
+ end, [{"incorrect",undefined},
+ {"Bad again",1},
+ {"bar",2}]).
+
%%--------------------------------------------------------------------
system_dir_option(Config) ->
DirUnread = proplists:get_value(unreadable_dir,Config),
--
cgit v1.2.3
From 193ccf4009eb346ca5dd43679b219e395016b03d Mon Sep 17 00:00:00 2001
From: Hans Nilsson
Date: Fri, 30 Oct 2015 11:33:40 +0100
Subject: ssh: enable users to give option keyboard_interact_fun
It is on purpose not documented. It needs more thinking before being finalized.
---
lib/ssh/src/ssh.erl | 4 ++++
1 file changed, 4 insertions(+)
(limited to 'lib')
diff --git a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl
index 6f79b48091..693b55a09e 100644
--- a/lib/ssh/src/ssh.erl
+++ b/lib/ssh/src/ssh.erl
@@ -337,6 +337,8 @@ handle_option([{pwdfun, _} = Opt | Rest], SocketOptions, SshOptions) ->
handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]);
handle_option([{key_cb, _} = Opt | Rest], SocketOptions, SshOptions) ->
handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]);
+handle_option([{keyboard_interact_fun, _} = Opt | Rest], SocketOptions, SshOptions) ->
+ handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]);
%%Backwards compatibility
handle_option([{allow_user_interaction, Value} | Rest], SocketOptions, SshOptions) ->
handle_option(Rest, SocketOptions, [handle_ssh_option({user_interaction, Value}) | SshOptions]);
@@ -468,6 +470,8 @@ handle_ssh_option({pwdfun, Value} = Opt) when is_function(Value,4) ->
Opt;
handle_ssh_option({key_cb, Value} = Opt) when is_atom(Value) ->
Opt;
+handle_ssh_option({keyboard_interact_fun, Value} = Opt) when is_function(Value,3) ->
+ Opt;
handle_ssh_option({compression, Value} = Opt) when is_atom(Value) ->
Opt;
handle_ssh_option({exec, {Module, Function, _}} = Opt) when is_atom(Module),
--
cgit v1.2.3
From 17517fb5ef4e9e7e6913a6eb4527f862ede29271 Mon Sep 17 00:00:00 2001
From: Hans Nilsson
Date: Fri, 30 Oct 2015 12:25:16 +0100
Subject: ssh: make corrections of keyboard-interactive client
* Newlines should be added after Name and Instructions field according to rfc4256.
* There was an error in the argument list of the last clause of ssh_auth:keyboard_interact_get_responses/9
* Correct client kb-interactive behaviour at auth failure
---
lib/ssh/src/ssh_auth.erl | 11 ++++++++---
lib/ssh/src/ssh_connection_handler.erl | 18 ++++++++++++++----
2 files changed, 22 insertions(+), 7 deletions(-)
(limited to 'lib')
diff --git a/lib/ssh/src/ssh_auth.erl b/lib/ssh/src/ssh_auth.erl
index 4272eb3c52..4967a2e4cd 100644
--- a/lib/ssh/src/ssh_auth.erl
+++ b/lib/ssh/src/ssh_auth.erl
@@ -364,6 +364,11 @@ method_preference(Algs) ->
[{"publickey", ?MODULE, publickey_msg, [A]} | Acc]
end,
[{"password", ?MODULE, password_msg, []},
+ {"keyboard-interactive", ?MODULE, keyboard_interactive_msg, []},
+ {"keyboard-interactive", ?MODULE, keyboard_interactive_msg, []},
+ {"keyboard-interactive", ?MODULE, keyboard_interactive_msg, []},
+ {"keyboard-interactive", ?MODULE, keyboard_interactive_msg, []},
+ {"keyboard-interactive", ?MODULE, keyboard_interactive_msg, []},
{"keyboard-interactive", ?MODULE, keyboard_interactive_msg, []}
],
Algs).
@@ -472,14 +477,14 @@ keyboard_interact_get_responses(false, undefined, undefined, _, _, _, [Prompt|_]
ssh_no_io:read_line(Prompt, Opts); %% Throws error as keyboard interaction is not allowed
keyboard_interact_get_responses(true, undefined, _,IoCb, Name, Instr, PromptInfos, Opts, _) ->
keyboard_interact(IoCb, Name, Instr, PromptInfos, Opts);
-keyboard_interact_get_responses(true, Fun, _, Name, Instr, PromptInfos, _, _, NumPrompts) ->
+keyboard_interact_get_responses(true, Fun, _Pwd, _IoCb, Name, Instr, PromptInfos, _Opts, NumPrompts) ->
keyboard_interact_fun(Fun, Name, Instr, PromptInfos, NumPrompts).
keyboard_interact(IoCb, Name, Instr, Prompts, Opts) ->
- if Name /= "" -> IoCb:format("~s", [Name]);
+ if Name /= "" -> IoCb:format("~s~n", [Name]);
true -> ok
end,
- if Instr /= "" -> IoCb:format("~s", [Instr]);
+ if Instr /= "" -> IoCb:format("~s~n", [Instr]);
true -> ok
end,
lists:map(fun({Prompt, true}) -> IoCb:read_line(Prompt, Opts);
diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl
index 7fb86c1108..a2d1b5b810 100644
--- a/lib/ssh/src/ssh_connection_handler.erl
+++ b/lib/ssh/src/ssh_connection_handler.erl
@@ -627,14 +627,24 @@ userauth_keyboard_interactive(#ssh_msg_userauth_info_response{} = Msg,
retry_fun(User, Address, Reason, Opts),
send_msg(Reply, State),
{next_state, userauth, next_packet(State#state{ssh_params = Ssh})}
- end.
-
+ end;
+userauth_keyboard_interactive(Msg = #ssh_msg_userauth_failure{},
+ #state{ssh_params = Ssh0 =
+ #ssh{role = client,
+ userauth_preference = Prefs0}}
+ = State) ->
+ Prefs = [{Method,M,F,A} || {Method,M,F,A} <- Prefs0,
+ Method =/= "keyboard-interactive"],
+ userauth(Msg, State#state{ssh_params = Ssh0#ssh{userauth_preference=Prefs}}).
+
-userauth_keyboard_interactive_info_response(Msg=#ssh_msg_userauth_failure{}, State) ->
+userauth_keyboard_interactive_info_response(Msg=#ssh_msg_userauth_failure{},
+ #state{ssh_params = #ssh{role = client}} = State) ->
userauth(Msg, State);
-userauth_keyboard_interactive_info_response(Msg=#ssh_msg_userauth_success{}, State) ->
+userauth_keyboard_interactive_info_response(Msg=#ssh_msg_userauth_success{},
+ #state{ssh_params = #ssh{role = client}} = State) ->
userauth(Msg, State).
%%--------------------------------------------------------------------
--
cgit v1.2.3
From 53bfbb61333af35cde29bb786817856925dcedf0 Mon Sep 17 00:00:00 2001
From: Hans Nilsson
Date: Mon, 2 Nov 2015 13:08:13 +0100
Subject: ssh: Document pwdfun
---
lib/ssh/doc/src/ssh.xml | 33 ++++++++++++++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)
(limited to 'lib')
diff --git a/lib/ssh/doc/src/ssh.xml b/lib/ssh/doc/src/ssh.xml
index 2b190c98b6..31a5e71401 100644
--- a/lib/ssh/doc/src/ssh.xml
+++ b/lib/ssh/doc/src/ssh.xml
@@ -471,12 +471,43 @@ kex is implicit but public_key is set explicitly.
- boolean()}]]>
+ boolean()}]]>
+ -
+
Provides a function for password validation. This could used for calling an external system or if
+ passwords should be stored as a hash. The fun returns:
+
+ - true if the user and password is valid and
+ - false otherwise.
+
+
+ This fun can also be used to make delays in authentication tries for example by calling
+ timer:sleep/1. To facilitate counting of failed tries
+ the State variable could be used. This state is per connection only. The first time the pwdfun
+ is called for a connection, the State variable has the value undefined.
+ The pwdfun can return - in addition to the values above - a new state
+ as:
+
+ - {true, NewState:any()} if the user and password is valid or
+ - {false, NewState:any()} if the user or password is invalid
+
+
+ A third usage is to block login attempts from a missbehaving peer. The State described above
+ can be used for this. In addition to the responses above, the following return value is introduced:
+
+ - disconnect if the connection should be closed immediately after sending a SSH_MSG_DISCONNECT
+ message.
+
+
+
+
+ boolean()}]]>
-
Provides a function for password validation. This function is called
with user and password as strings, and returns
if the password is valid and
otherwise.
+ This option ({pwdfun,fun/2}) is the same as a subset of the previous
+ ({pwdfun,fun/4}). It is kept for compatibility.
--
cgit v1.2.3
From 19f3eafbb237af7b6a9d81ebbddae19c41418f8b Mon Sep 17 00:00:00 2001
From: Hans Nilsson
Date: Tue, 3 Nov 2015 14:53:30 +0100
Subject: ssh: changes after doc review
---
lib/ssh/doc/src/ssh.xml | 25 ++++++++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
(limited to 'lib')
diff --git a/lib/ssh/doc/src/ssh.xml b/lib/ssh/doc/src/ssh.xml
index 31a5e71401..f530a68dd9 100644
--- a/lib/ssh/doc/src/ssh.xml
+++ b/lib/ssh/doc/src/ssh.xml
@@ -60,6 +60,29 @@
+
+ OPTIONS
+ The exact behaviour of some functions can be adjusted with the use of options which are documented together
+ with the functions. Generally could each option be used at most one time in each function call. If given two or more
+ times, the effect is not predictable unless explicitly documented.
+ The options are of different kinds:
+
+ Limits
+ which alters limits in the system, for example number of simultaneous login attempts.
+
+ Timeouts
+ which give some defined behaviour if too long time elapses before a given event or action,
+ for example time to wait for an answer.
+
+ Callbacks
+ which gives the caller of the function the possibility to execute own code on some events,
+ for example calling an own logging function or to perform an own login function
+
+ Behaviour
+ which changes the systems behaviour.
+
+
+
DATA TYPES
Type definitions that are used more than once in
@@ -471,7 +494,7 @@ kex is implicit but public_key is set explicitly.
- boolean()}]]>
+ boolean() | disconnect | {boolean(),any()} }]]>
-
Provides a function for password validation. This could used for calling an external system or if
passwords should be stored as a hash. The fun returns:
--
cgit v1.2.3