Skip to content

Commit

Permalink
Add support for keyboard interactive authentication to DemoServer
Browse files Browse the repository at this point in the history
Super-basic, it still hardcodes the prompts.
  • Loading branch information
JamesWrigley committed Jan 5, 2024
1 parent 98d9b43 commit bb46328
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 3 deletions.
17 changes: 15 additions & 2 deletions gen/gen.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import Clang.Generators: ExprNode, AbstractFunctionNodeType
ctx_objects = Dict{Symbol, Any}()

# These are lists of functions that we'll rewrite to return Julia types
string_functions = [:ssh_message_auth_user, :ssh_message_auth_password]
string_functions = [:ssh_message_auth_user, :ssh_message_auth_password,
:ssh_userauth_kbdint_getanswer]
bool_functions = [:ssh_message_auth_kbdint_is_response]
all_rewritable_functions = vcat(string_functions, bool_functions)
ssh_ok_functions = [:ssh_message_auth_reply_success, :ssh_message_auth_set_methods,
:ssh_message_reply_default]
all_rewritable_functions = vcat(string_functions, bool_functions, ssh_ok_functions)

"""
Helper function to generate documentation for symbols with missing docstrings.
Expand Down Expand Up @@ -115,6 +118,16 @@ function rewrite!(ctx)
elseif name in bool_functions
wrapper = :(return ret == 1)
ret_type = Bool
elseif name in ssh_ok_functions
wrapper = quote
if ret != SSH_OK
# This ugly concatenation is necessary because we
# have to interpolate the function name into the
# error string but also keep the return value
# interpolation from being escaped.
throw(LibSSHException($("Error from $name, did not return SSH_OK: ") * "$(ret)"))
end
end
end

if !isnothing(wrapper)
Expand Down
50 changes: 50 additions & 0 deletions src/bindings.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4898,6 +4898,32 @@ function ssh_get_log_callback()
@ccall libssh.ssh_get_log_callback()::ssh_logging_callback
end

"""
userauth_kbdint_getanswer(session, i)::String
Auto-generated wrapper around [`ssh_userauth_kbdint_getanswer`](@ref).
"""
function userauth_kbdint_getanswer(session, i)::String
ret = ssh_userauth_kbdint_getanswer(session, i)
if ret == C_NULL
throw(LibSSHException("Error from ssh_userauth_kbdint_getanswer, no string found (returned C_NULL)"))
else
return unsafe_string(Ptr{UInt8}(ret))
end
end

"""
message_reply_default(msg)
Auto-generated wrapper around [`ssh_message_reply_default`](@ref).
"""
function message_reply_default(msg)
ret = ssh_message_reply_default(msg)
if ret != SSH_OK
throw(LibSSHException("Error from ssh_message_reply_default, did not return SSH_OK: " * "$(ret)"))
end
end

"""
message_auth_user(msg)::String
Expand Down Expand Up @@ -4936,6 +4962,30 @@ function message_auth_kbdint_is_response(msg)::Bool
return ret == 1
end

"""
message_auth_reply_success(msg, partial)
Auto-generated wrapper around [`ssh_message_auth_reply_success`](@ref).
"""
function message_auth_reply_success(msg, partial)
ret = ssh_message_auth_reply_success(msg, partial)
if ret != SSH_OK
throw(LibSSHException("Error from ssh_message_auth_reply_success, did not return SSH_OK: " * "$(ret)"))
end
end

"""
message_auth_set_methods(msg, methods)
Auto-generated wrapper around [`ssh_message_auth_set_methods`](@ref).
"""
function message_auth_set_methods(msg, methods)
ret = ssh_message_auth_set_methods(msg, methods)
if ret != SSH_OK
throw(LibSSHException("Error from ssh_message_auth_set_methods, did not return SSH_OK: " * "$(ret)"))
end
end

# Skipping MacroDefinition: LIBSSH_API __attribute__ ( ( visibility ( "default" ) ) )

# Skipping MacroDefinition: SSH_DEPRECATED __attribute__ ( ( deprecated ) )
Expand Down
113 changes: 112 additions & 1 deletion src/server.jl
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,16 @@ end
"""
$(TYPEDSIGNATURES)
Wrapper around [`LibSSH.lib.message_auth_set_methods`](@ref).
"""
function set_auth_methods(msg::lib.ssh_message, auth_methods::Vector{AuthMethod})
bitflag = reduce(|, Int.(auth_methods))
lib.message_auth_set_methods(msg, bitflag)
end

"""
$(TYPEDSIGNATURES)
Non-blocking wrapper around `LibSSH.lib.ssh_handle_key_exchange()`. Returns
`true` or `false` depending on whether the exchange succeeded.
"""
Expand Down Expand Up @@ -411,10 +421,25 @@ $(TYPEDSIGNATURES)
Set message callbacks for the sessions accepted by a Server. This must be set
before `listen()` is called to take effect. `listen()` will automatically set
the callback before passing the session to the user handler.
The callback function must have the signature:
f(session::Session, msg::lib.ssh_message, userdata)::Bool
The return value indicates whether further handling of the message is necessary.
It should be `true` if the message wasn't handled or needs to be handled by
libssh, or `false` if the message was completely handled and doesn't need any
more action from libssh.
"""
function set_message_callback(f::Function, server::Server, userdata)
if !hasmethod(f, (Session, lib.ssh_message, typeof(userdata)))
throw(ArgumentError("Callback function f() doesn't have the right signature"))
end

server._message_callback = f
server._message_callback_userdata = userdata

return nothing
end

"""
Expand All @@ -439,6 +464,50 @@ function event_dopoll(event::SshEvent, session::Session, sshchan_locks...)
return ret
end

"""
$(TYPEDSIGNATURES)
## Parameters
- `msg`: The message to reply to.
- `name`: The name of the message block.
- `instruction`: The instruction for the user.
- `prompts`: The prompts to show to the user.
- `echo`: Whether the client should echo the answer to the prompts (e.g. it
probably shouldn't echo the password).
Wrapper around [`lib.ssh_message_auth_interactive_request`](@ref).
"""
function message_auth_interactive_request(msg::lib.ssh_message,
name::AbstractString, instruction::AbstractString,
prompts::Vector{String}, echo::Vector{Bool})
# Check that prompts and echo have the same length
if length(prompts) != length(echo)
throw(ArgumentError("`prompts` and `echo` must have the same length! Actual lengths are $(length(prompts)) and $(length(echo))"))
end

# Convert arguments to C types
name_cstr = Base.cconvert(Cstring, name)
instruction_cstr = Base.cconvert(Cstring, instruction)
prompts_cstrs = [Base.cconvert(Cstring, p) for p in prompts]
echo_arr = map(Cchar, echo)

# Call library
GC.@preserve prompts_cstrs echo_arr begin
prompts_arr = pointer.(prompts_cstrs)

ret = lib.ssh_message_auth_interactive_request(msg, name_cstr, instruction_cstr,
length(prompts), Ptr{Ptr{UInt8}}(pointer(prompts_arr)),
pointer(echo_arr))
end

if ret == SSH_ERROR
throw(LibSSHException("Error when responding to kbdint auth request: $(ret)"))
end

return ret
end


module Demo

Expand Down Expand Up @@ -483,8 +552,9 @@ end

function on_auth_password(session, user, password, demo_server)::ssh.AuthStatus
_add_log_event!(demo_server, :auth_password, (user, password))
demo_server.authenticated = password == demo_server.password

return password == demo_server.password ? ssh.AuthStatus_Success : ssh.AuthStatus_Denied
return demo_server.authenticated ? ssh.AuthStatus_Success : ssh.AuthStatus_Denied
end

function on_auth_none(session, user, demo_server)::ssh.AuthStatus
Expand Down Expand Up @@ -608,6 +678,46 @@ function on_message(session, msg::lib.ssh_message, demo_server)::Bool
return false
end

# Handle keyboard-interactive authentication
if msg_type == ssh.RequestType_Auth && msg_subtype == lib.SSH_AUTH_METHOD_INTERACTIVE
if demo_server.authenticated
_add_log_event!(demo_server, :auth_kbdint, "already authenticated")
lib.message_auth_reply_success(msg, Int(false))
return false
end

if !lib.message_auth_kbdint_is_response(msg)
# This means the user is requesting authentication
user = lib.message_auth_user(msg)
_add_log_event!(demo_server, :auth_kbdint, user)
ssh.message_auth_interactive_request(msg, "Demo server login", "Enter your details.",
["Password: ", "Token: "], [true, true])
return false
else
# Now they're responding to our prompts
n_answers = lib.ssh_userauth_kbdint_getnanswers(session.ptr)

# If they didn't return the correct number of answers, deny the request
if n_answers != 2
_add_log_event!(demo_server, :auth_kbdint, "denied")
lib.message_reply_default(msg)
return false
end

# Get the answers and check them
password = lib.userauth_kbdint_getanswer(session.ptr, 0)
token = lib.userauth_kbdint_getanswer(session.ptr, 1)
if password == "foo" && token == "bar"
_add_log_event!(demo_server, :auth_kbdint, "accepted with '$password' and '$token'")
lib.message_auth_reply_success(msg, Int(false))
demo_server.authenticated = true
return false
end

return true
end
end

return true
end

Expand Down Expand Up @@ -646,6 +756,7 @@ end
sshchan::Union{ssh.SshChannel, Nothing} = nothing
verbose::Bool = false
password::Union{String, Nothing} = nothing
authenticated::Bool = false

exec_task::Union{Task, Nothing} = nothing
exec_proc::Union{Base.Process, Nothing} = nothing
Expand Down
2 changes: 2 additions & 0 deletions src/session.jl
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ $(TYPEDSIGNATURES)
Wrapper around `LibSSH.lib.ssh_userauth_list()`. It will throw a
`LibSSHException` if the SSH server supports `AuthMethod_None` or if another
error occurred.
This wrapper will automatically call [`userauth_none()`](@ref) beforehand.
"""
function userauth_list(session::Session)
# First we have to call ssh_userauth_none() for... some reason, according to
Expand Down
16 changes: 16 additions & 0 deletions test/LibSSHTests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ end
# Check that the authentication methods were called
@test logs[:auth_none] == [true]
@test logs[:auth_password] == [("foo", "bar")]
@test demo_server.authenticated

# And a channel was created
@test !isnothing(demo_server.sshchan)
Expand Down Expand Up @@ -146,6 +147,21 @@ end

@test demo_server.callback_log[:message_request] == [(ssh.RequestType_ChannelOpen, lib.SSH_CHANNEL_DIRECT_TCPIP)]
end

@testset "Keyboard-interactive authentication" begin
demo_server = DemoServer(2222; auth_methods=[ssh.AuthMethod_Interactive]) do
# Run the script
script_path = joinpath(@__DIR__, "interactive_ssh.sh")
proc = run(`expect -f $script_path`; wait=false)
wait(proc)
end

# Check that authentication succeeded
@test demo_server.authenticated

# And the command was executed
@test demo_server.callback_log[:channel_exec_request] == ["whoami"]
end
end

@testset "Session" begin
Expand Down
18 changes: 18 additions & 0 deletions test/interactive_ssh.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#! /usr/bin/expect

set timeout 5

# Start the SSH process
spawn ssh -o NoHostAuthenticationForLocalhost=yes -p 2222 localhost whoami

# Pass the correct prompts
expect "Password:" {
send "foo\r"

expect "Token:" {
send "bar\r"
}
}

# Wait for the command to finish
wait

0 comments on commit bb46328

Please sign in to comment.