mirror of
https://git.libssh.org/projects/libssh.git
synced 2026-02-04 20:30:38 +09:00
Originally reported by Till on mailing list here:
https://archive.libssh.org/libssh/2025-05/0000000.html
After some debugging, it turns out the client code does not guarantee
the extensions are processed before making decisions on the signature algorithm
that is being used for authentication, causing false-positive failures.
This does not happen in the tests, where we initially call ssh_userauth_none,
which enumerates authentications methods and as a side effect processes
outstanding packets such as SSH_EXT_INFO message with the server-sig-algs
extension.
When the first function called after `ssh_connect()` is
`ssh_userauth_publickey()`, the `ssh_userauth_request_service()` was wrongly
called only after the signature algorithm compatibility was checked.
Signed-off-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Norbert Pocs <norbertpocs0@gmail.com>
(cherry picked from commit 12baa5200a)
2361 lines
76 KiB
C
2361 lines
76 KiB
C
/*
|
|
* auth.c - Authentication with SSH protocols
|
|
*
|
|
* This file is part of the SSH Library
|
|
*
|
|
* Copyright (c) 2003-2013 by Aris Adamantiadis <aris@0xbadc0de.be>
|
|
* Copyright (c) 2008-2013 Andreas Schneider <asn@cryptomilk.org>
|
|
*
|
|
* The SSH Library is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or (at your
|
|
* option) any later version.
|
|
*
|
|
* The SSH Library is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
|
* License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with the SSH Library; see the file COPYING. If not, write to
|
|
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
|
|
* MA 02111-1307, USA.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#ifndef _WIN32
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#endif
|
|
|
|
#include "libssh/priv.h"
|
|
#include "libssh/crypto.h"
|
|
#include "libssh/ssh2.h"
|
|
#include "libssh/buffer.h"
|
|
#include "libssh/agent.h"
|
|
#include "libssh/misc.h"
|
|
#include "libssh/packet.h"
|
|
#include "libssh/session.h"
|
|
#include "libssh/keys.h"
|
|
#include "libssh/auth.h"
|
|
#include "libssh/pki.h"
|
|
#include "libssh/gssapi.h"
|
|
#include "libssh/legacy.h"
|
|
|
|
/**
|
|
* @defgroup libssh_auth The SSH authentication functions
|
|
* @ingroup libssh
|
|
*
|
|
* Functions to authenticate with a server.
|
|
*
|
|
* @{
|
|
*/
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* @brief Ask for access to the ssh-userauth service.
|
|
*
|
|
* @param[in] session The SSH session handle.
|
|
*
|
|
* @returns SSH_OK on success, SSH_ERROR on error.
|
|
* @returns SSH_AGAIN on nonblocking mode, if calling that function
|
|
* again is necessary
|
|
*/
|
|
static int ssh_userauth_request_service(ssh_session session)
|
|
{
|
|
int rc;
|
|
|
|
rc = ssh_service_request(session, "ssh-userauth");
|
|
if ((rc != SSH_OK) && (rc != SSH_AGAIN)) {
|
|
SSH_LOG(SSH_LOG_TRACE,
|
|
"Failed to request \"ssh-userauth\" service");
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
static int ssh_auth_response_termination(void *user)
|
|
{
|
|
ssh_session session = (ssh_session)user;
|
|
switch (session->auth.state) {
|
|
case SSH_AUTH_STATE_NONE:
|
|
case SSH_AUTH_STATE_KBDINT_SENT:
|
|
case SSH_AUTH_STATE_GSSAPI_REQUEST_SENT:
|
|
case SSH_AUTH_STATE_GSSAPI_TOKEN:
|
|
case SSH_AUTH_STATE_GSSAPI_MIC_SENT:
|
|
case SSH_AUTH_STATE_PUBKEY_AUTH_SENT:
|
|
case SSH_AUTH_STATE_PUBKEY_OFFER_SENT:
|
|
case SSH_AUTH_STATE_PASSWORD_AUTH_SENT:
|
|
case SSH_AUTH_STATE_AUTH_NONE_SENT:
|
|
return 0;
|
|
default:
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
static const char *ssh_auth_get_current_method(ssh_session session)
|
|
{
|
|
const char *method = "unknown";
|
|
|
|
switch (session->auth.current_method) {
|
|
case SSH_AUTH_METHOD_NONE:
|
|
method = "none";
|
|
break;
|
|
case SSH_AUTH_METHOD_PASSWORD:
|
|
method = "password";
|
|
break;
|
|
case SSH_AUTH_METHOD_PUBLICKEY:
|
|
method = "publickey";
|
|
break;
|
|
case SSH_AUTH_METHOD_HOSTBASED:
|
|
method = "hostbased";
|
|
break;
|
|
case SSH_AUTH_METHOD_INTERACTIVE:
|
|
method = "keyboard interactive";
|
|
break;
|
|
case SSH_AUTH_METHOD_GSSAPI_MIC:
|
|
method = "gssapi";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return method;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* @brief Wait for a response of an authentication function.
|
|
*
|
|
* @param[in] session The SSH session.
|
|
*
|
|
* @returns SSH_AUTH_SUCCESS Authentication success, or pubkey accepted
|
|
* SSH_AUTH_PARTIAL Authentication succeeded but another mean
|
|
* of authentication is needed.
|
|
* SSH_AUTH_INFO Data for keyboard-interactive
|
|
* SSH_AUTH_AGAIN In nonblocking mode, call has to be made again
|
|
* SSH_AUTH_ERROR Error during the process.
|
|
*/
|
|
static int ssh_userauth_get_response(ssh_session session)
|
|
{
|
|
int rc = SSH_AUTH_ERROR;
|
|
|
|
rc = ssh_handle_packets_termination(session, SSH_TIMEOUT_USER,
|
|
ssh_auth_response_termination, session);
|
|
if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
if (!ssh_auth_response_termination(session)) {
|
|
return SSH_AUTH_AGAIN;
|
|
}
|
|
|
|
switch(session->auth.state) {
|
|
case SSH_AUTH_STATE_ERROR:
|
|
rc = SSH_AUTH_ERROR;
|
|
break;
|
|
case SSH_AUTH_STATE_FAILED:
|
|
rc = SSH_AUTH_DENIED;
|
|
break;
|
|
case SSH_AUTH_STATE_INFO:
|
|
rc = SSH_AUTH_INFO;
|
|
break;
|
|
case SSH_AUTH_STATE_PARTIAL:
|
|
rc = SSH_AUTH_PARTIAL;
|
|
break;
|
|
case SSH_AUTH_STATE_PK_OK:
|
|
case SSH_AUTH_STATE_SUCCESS:
|
|
rc = SSH_AUTH_SUCCESS;
|
|
break;
|
|
case SSH_AUTH_STATE_KBDINT_SENT:
|
|
case SSH_AUTH_STATE_GSSAPI_REQUEST_SENT:
|
|
case SSH_AUTH_STATE_GSSAPI_TOKEN:
|
|
case SSH_AUTH_STATE_GSSAPI_MIC_SENT:
|
|
case SSH_AUTH_STATE_PUBKEY_OFFER_SENT:
|
|
case SSH_AUTH_STATE_PUBKEY_AUTH_SENT:
|
|
case SSH_AUTH_STATE_PASSWORD_AUTH_SENT:
|
|
case SSH_AUTH_STATE_AUTH_NONE_SENT:
|
|
case SSH_AUTH_STATE_NONE:
|
|
/* not reached */
|
|
rc = SSH_AUTH_ERROR;
|
|
break;
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* @brief Handles a SSH_USERAUTH_BANNER packet.
|
|
*
|
|
* This banner should be shown to user prior to authentication
|
|
*/
|
|
SSH_PACKET_CALLBACK(ssh_packet_userauth_banner) {
|
|
ssh_string banner;
|
|
(void)type;
|
|
(void)user;
|
|
|
|
banner = ssh_buffer_get_ssh_string(packet);
|
|
if (banner == NULL) {
|
|
SSH_LOG(SSH_LOG_TRACE,
|
|
"Invalid SSH_USERAUTH_BANNER packet");
|
|
} else {
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Received SSH_USERAUTH_BANNER packet");
|
|
if (session->banner != NULL)
|
|
SSH_STRING_FREE(session->banner);
|
|
session->banner = banner;
|
|
}
|
|
|
|
return SSH_PACKET_USED;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* @brief Handles a SSH_USERAUTH_FAILURE packet.
|
|
*
|
|
* This handles the complete or partial authentication failure.
|
|
*/
|
|
SSH_PACKET_CALLBACK(ssh_packet_userauth_failure) {
|
|
const char *current_method = ssh_auth_get_current_method(session);
|
|
char *auth_methods = NULL;
|
|
uint8_t partial = 0;
|
|
int rc;
|
|
(void) type;
|
|
(void) user;
|
|
|
|
rc = ssh_buffer_unpack(packet, "sb", &auth_methods, &partial);
|
|
if (rc != SSH_OK) {
|
|
ssh_set_error(session, SSH_FATAL,
|
|
"Invalid SSH_MSG_USERAUTH_FAILURE message");
|
|
session->auth.state = SSH_AUTH_STATE_ERROR;
|
|
goto end;
|
|
}
|
|
|
|
if (partial) {
|
|
session->auth.state = SSH_AUTH_STATE_PARTIAL;
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Partial success for '%s'. Authentication that can continue: %s",
|
|
current_method,
|
|
auth_methods);
|
|
} else {
|
|
session->auth.state = SSH_AUTH_STATE_FAILED;
|
|
ssh_set_error(session, SSH_REQUEST_DENIED,
|
|
"Access denied for '%s'. Authentication that can continue: %s",
|
|
current_method,
|
|
auth_methods);
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"%s",
|
|
ssh_get_error(session));
|
|
|
|
}
|
|
session->auth.supported_methods = 0;
|
|
if (strstr(auth_methods, "password") != NULL) {
|
|
session->auth.supported_methods |= SSH_AUTH_METHOD_PASSWORD;
|
|
}
|
|
if (strstr(auth_methods, "keyboard-interactive") != NULL) {
|
|
session->auth.supported_methods |= SSH_AUTH_METHOD_INTERACTIVE;
|
|
}
|
|
if (strstr(auth_methods, "publickey") != NULL) {
|
|
session->auth.supported_methods |= SSH_AUTH_METHOD_PUBLICKEY;
|
|
}
|
|
if (strstr(auth_methods, "hostbased") != NULL) {
|
|
session->auth.supported_methods |= SSH_AUTH_METHOD_HOSTBASED;
|
|
}
|
|
if (strstr(auth_methods, "gssapi-with-mic") != NULL) {
|
|
session->auth.supported_methods |= SSH_AUTH_METHOD_GSSAPI_MIC;
|
|
}
|
|
|
|
end:
|
|
session->auth.current_method = SSH_AUTH_METHOD_UNKNOWN;
|
|
SAFE_FREE(auth_methods);
|
|
|
|
return SSH_PACKET_USED;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* @brief Handles a SSH_USERAUTH_SUCCESS packet.
|
|
*
|
|
* It is also used to communicate the new to the upper levels.
|
|
*/
|
|
SSH_PACKET_CALLBACK(ssh_packet_userauth_success)
|
|
{
|
|
struct ssh_crypto_struct *crypto = NULL;
|
|
|
|
(void)packet;
|
|
(void)type;
|
|
(void)user;
|
|
|
|
SSH_LOG(SSH_LOG_DEBUG, "Authentication successful");
|
|
SSH_LOG(SSH_LOG_TRACE, "Received SSH_USERAUTH_SUCCESS");
|
|
|
|
session->auth.state = SSH_AUTH_STATE_SUCCESS;
|
|
session->session_state = SSH_SESSION_STATE_AUTHENTICATED;
|
|
session->flags |= SSH_SESSION_FLAG_AUTHENTICATED;
|
|
|
|
crypto = ssh_packet_get_current_crypto(session, SSH_DIRECTION_OUT);
|
|
if (crypto != NULL && crypto->delayed_compress_out) {
|
|
SSH_LOG(SSH_LOG_DEBUG, "Enabling delayed compression OUT");
|
|
crypto->do_compress_out = 1;
|
|
}
|
|
|
|
crypto = ssh_packet_get_current_crypto(session, SSH_DIRECTION_IN);
|
|
if (crypto != NULL && crypto->delayed_compress_in) {
|
|
SSH_LOG(SSH_LOG_DEBUG, "Enabling delayed compression IN");
|
|
crypto->do_compress_in = 1;
|
|
}
|
|
|
|
/* Reset errors by previous authentication methods. */
|
|
ssh_reset_error(session);
|
|
session->auth.current_method = SSH_AUTH_METHOD_UNKNOWN;
|
|
return SSH_PACKET_USED;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* @brief Handles a SSH_USERAUTH_PK_OK or SSH_USERAUTH_INFO_REQUEST packet.
|
|
*
|
|
* Since the two types of packets share the same code, additional work is done
|
|
* to understand if we are in a public key or keyboard-interactive context.
|
|
*/
|
|
SSH_PACKET_CALLBACK(ssh_packet_userauth_pk_ok) {
|
|
int rc;
|
|
|
|
SSH_LOG(SSH_LOG_TRACE,
|
|
"Received SSH_USERAUTH_PK_OK/INFO_REQUEST/GSSAPI_RESPONSE");
|
|
|
|
if (session->auth.state == SSH_AUTH_STATE_KBDINT_SENT) {
|
|
/* Assuming we are in keyboard-interactive context */
|
|
SSH_LOG(SSH_LOG_TRACE,
|
|
"keyboard-interactive context, "
|
|
"assuming SSH_USERAUTH_INFO_REQUEST");
|
|
rc = ssh_packet_userauth_info_request(session,type,packet,user);
|
|
#ifdef WITH_GSSAPI
|
|
} else if (session->auth.state == SSH_AUTH_STATE_GSSAPI_REQUEST_SENT) {
|
|
rc = ssh_packet_userauth_gssapi_response(session, type, packet, user);
|
|
#endif
|
|
} else if (session->auth.state == SSH_AUTH_STATE_PUBKEY_OFFER_SENT) {
|
|
session->auth.state = SSH_AUTH_STATE_PK_OK;
|
|
SSH_LOG(SSH_LOG_TRACE, "Assuming SSH_USERAUTH_PK_OK");
|
|
rc = SSH_PACKET_USED;
|
|
} else {
|
|
session->auth.state = SSH_AUTH_STATE_ERROR;
|
|
SSH_LOG(SSH_LOG_TRACE, "SSH_USERAUTH_PK_OK received in wrong state");
|
|
rc = SSH_PACKET_USED;
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* @brief Get available authentication methods from the server.
|
|
*
|
|
* This requires the function ssh_userauth_none() to be called before the
|
|
* methods are available. The server MAY return a list of methods that may
|
|
* continue.
|
|
*
|
|
* @param[in] session The SSH session.
|
|
*
|
|
* @param[in] username Deprecated, set to NULL.
|
|
*
|
|
* @returns A bitfield of the following values:
|
|
* - SSH_AUTH_METHOD_PASSWORD
|
|
* - SSH_AUTH_METHOD_PUBLICKEY
|
|
* - SSH_AUTH_METHOD_HOSTBASED
|
|
* - SSH_AUTH_METHOD_INTERACTIVE
|
|
*
|
|
* @warning Other reserved flags may appear in future versions.
|
|
* @see ssh_userauth_none()
|
|
*/
|
|
int ssh_userauth_list(ssh_session session, const char *username)
|
|
{
|
|
(void) username; /* unused */
|
|
|
|
if (session == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
return session->auth.supported_methods;
|
|
}
|
|
|
|
/**
|
|
* @brief Try to authenticate through the "none" method.
|
|
*
|
|
* @param[in] session The ssh session to use.
|
|
*
|
|
* @param[in] username The username, this SHOULD be NULL.
|
|
*
|
|
* @returns SSH_AUTH_ERROR: A serious error happened.\n
|
|
* SSH_AUTH_DENIED: Authentication failed: use another method\n
|
|
* SSH_AUTH_PARTIAL: You've been partially authenticated, you still
|
|
* have to use another method\n
|
|
* SSH_AUTH_SUCCESS: Authentication success\n
|
|
* SSH_AUTH_AGAIN: In nonblocking mode, you've got to call this again
|
|
* later.
|
|
*
|
|
* @note Most server implementations do not permit changing the username during
|
|
* authentication. The username should only be set with ssh_options_set() only
|
|
* before you connect to the server.
|
|
*/
|
|
int ssh_userauth_none(ssh_session session, const char *username)
|
|
{
|
|
int rc;
|
|
|
|
switch (session->pending_call_state) {
|
|
case SSH_PENDING_CALL_NONE:
|
|
break;
|
|
case SSH_PENDING_CALL_AUTH_NONE:
|
|
goto pending;
|
|
default:
|
|
ssh_set_error(session, SSH_FATAL,
|
|
"Wrong state (%d) during pending SSH call",
|
|
session->pending_call_state);
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
rc = ssh_userauth_request_service(session);
|
|
if (rc == SSH_AGAIN) {
|
|
return SSH_AUTH_AGAIN;
|
|
} else if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/* request */
|
|
rc = ssh_buffer_pack(session->out_buffer, "bsss",
|
|
SSH2_MSG_USERAUTH_REQUEST,
|
|
username ? username : session->opts.username,
|
|
"ssh-connection",
|
|
"none"
|
|
);
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
session->auth.current_method = SSH_AUTH_METHOD_NONE;
|
|
session->auth.state = SSH_AUTH_STATE_AUTH_NONE_SENT;
|
|
session->pending_call_state = SSH_PENDING_CALL_AUTH_NONE;
|
|
rc = ssh_packet_send(session);
|
|
if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
pending:
|
|
rc = ssh_userauth_get_response(session);
|
|
if (rc != SSH_AUTH_AGAIN) {
|
|
session->pending_call_state = SSH_PENDING_CALL_NONE;
|
|
}
|
|
|
|
return rc;
|
|
fail:
|
|
ssh_set_error_oom(session);
|
|
ssh_buffer_reinit(session->out_buffer);
|
|
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/**
|
|
* @brief Try to authenticate with the given public key.
|
|
*
|
|
* To avoid unnecessary processing and user interaction, the following method
|
|
* is provided for querying whether authentication using the 'pubkey' would
|
|
* be possible.
|
|
*
|
|
* @param[in] session The SSH session.
|
|
*
|
|
* @param[in] username The username, this SHOULD be NULL.
|
|
*
|
|
* @param[in] pubkey The public key to try.
|
|
*
|
|
* @return SSH_AUTH_ERROR: A serious error happened.\n
|
|
* SSH_AUTH_DENIED: The server doesn't accept that public key as an
|
|
* authentication token. Try another key or another
|
|
* method.\n
|
|
* SSH_AUTH_PARTIAL: You've been partially authenticated, you still
|
|
* have to use another method.\n
|
|
* SSH_AUTH_SUCCESS: The public key is accepted, you want now to use
|
|
* ssh_userauth_publickey().\n
|
|
* SSH_AUTH_AGAIN: In nonblocking mode, you've got to call this again
|
|
* later.
|
|
*
|
|
* @note Most server implementations do not permit changing the username during
|
|
* authentication. The username should only be set with ssh_options_set() only
|
|
* before you connect to the server.
|
|
*/
|
|
int ssh_userauth_try_publickey(ssh_session session,
|
|
const char *username,
|
|
const ssh_key pubkey)
|
|
{
|
|
ssh_string pubkey_s = NULL;
|
|
const char *sig_type_c = NULL;
|
|
bool allowed;
|
|
int rc;
|
|
|
|
if (session == NULL) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
if (pubkey == NULL || !ssh_key_is_public(pubkey)) {
|
|
ssh_set_error(session, SSH_FATAL, "Invalid pubkey");
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
switch(session->pending_call_state) {
|
|
case SSH_PENDING_CALL_NONE:
|
|
break;
|
|
case SSH_PENDING_CALL_AUTH_OFFER_PUBKEY:
|
|
goto pending;
|
|
default:
|
|
ssh_set_error(session,
|
|
SSH_FATAL,
|
|
"Wrong state (%d) during pending SSH call",
|
|
session->pending_call_state);
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/* Note, that this is intentionally before checking the signature type
|
|
* compatibility to make sure the possible EXT_INFO packet is processed,
|
|
* extensions recorded and the right signature type is used below
|
|
*/
|
|
rc = ssh_userauth_request_service(session);
|
|
if (rc == SSH_AGAIN) {
|
|
return SSH_AUTH_AGAIN;
|
|
} else if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/* Check if the given public key algorithm is allowed */
|
|
sig_type_c = ssh_key_get_signature_algorithm(session, pubkey->type);
|
|
if (sig_type_c == NULL) {
|
|
ssh_set_error(session, SSH_REQUEST_DENIED,
|
|
"Invalid key type (unknown)");
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
rc = ssh_key_algorithm_allowed(session, sig_type_c);
|
|
if (!rc) {
|
|
ssh_set_error(session, SSH_REQUEST_DENIED,
|
|
"The key algorithm '%s' is not allowed to be used by"
|
|
" PUBLICKEY_ACCEPTED_TYPES configuration option",
|
|
sig_type_c);
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
allowed = ssh_key_size_allowed(session, pubkey);
|
|
if (!allowed) {
|
|
ssh_set_error(session, SSH_REQUEST_DENIED,
|
|
"The '%s' key type of size %d is not allowed by "
|
|
"RSA_MIN_SIZE", sig_type_c, ssh_key_size(pubkey));
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
|
|
/* public key */
|
|
rc = ssh_pki_export_pubkey_blob(pubkey, &pubkey_s);
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
SSH_LOG(SSH_LOG_TRACE, "Trying signature type %s", sig_type_c);
|
|
/* request */
|
|
rc = ssh_buffer_pack(session->out_buffer, "bsssbsS",
|
|
SSH2_MSG_USERAUTH_REQUEST,
|
|
username ? username : session->opts.username,
|
|
"ssh-connection",
|
|
"publickey",
|
|
0, /* private key ? */
|
|
sig_type_c, /* algo */
|
|
pubkey_s /* public key */
|
|
);
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
SSH_STRING_FREE(pubkey_s);
|
|
|
|
session->auth.current_method = SSH_AUTH_METHOD_PUBLICKEY;
|
|
session->auth.state = SSH_AUTH_STATE_PUBKEY_OFFER_SENT;
|
|
session->pending_call_state = SSH_PENDING_CALL_AUTH_OFFER_PUBKEY;
|
|
rc = ssh_packet_send(session);
|
|
if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
pending:
|
|
rc = ssh_userauth_get_response(session);
|
|
if (rc != SSH_AUTH_AGAIN) {
|
|
session->pending_call_state = SSH_PENDING_CALL_NONE;
|
|
}
|
|
|
|
return rc;
|
|
fail:
|
|
SSH_STRING_FREE(pubkey_s);
|
|
ssh_set_error_oom(session);
|
|
ssh_buffer_reinit(session->out_buffer);
|
|
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/**
|
|
* @brief Authenticate with public/private key or certificate.
|
|
*
|
|
* @param[in] session The SSH session.
|
|
*
|
|
* @param[in] username The username, this SHOULD be NULL.
|
|
*
|
|
* @param[in] privkey The private key for authentication.
|
|
*
|
|
* @return SSH_AUTH_ERROR: A serious error happened.\n
|
|
* SSH_AUTH_DENIED: The server doesn't accept that public key as an
|
|
* authentication token. Try another key or another
|
|
* method.\n
|
|
* SSH_AUTH_PARTIAL: You've been partially authenticated, you still
|
|
* have to use another method.\n
|
|
* SSH_AUTH_SUCCESS: The public key is accepted.\n
|
|
* SSH_AUTH_AGAIN: In nonblocking mode, you've got to call this again
|
|
* later.
|
|
*
|
|
* @note Most server implementations do not permit changing the username during
|
|
* authentication. The username should only be set with ssh_options_set() only
|
|
* before you connect to the server.
|
|
*/
|
|
int ssh_userauth_publickey(ssh_session session,
|
|
const char *username,
|
|
const ssh_key privkey)
|
|
{
|
|
ssh_string str = NULL;
|
|
bool allowed;
|
|
int rc;
|
|
const char *sig_type_c = NULL;
|
|
enum ssh_keytypes_e key_type;
|
|
enum ssh_digest_e hash_type;
|
|
|
|
if (session == NULL) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
if (privkey == NULL || !ssh_key_is_private(privkey)) {
|
|
ssh_set_error(session, SSH_FATAL, "Invalid private key");
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
switch(session->pending_call_state) {
|
|
case SSH_PENDING_CALL_NONE:
|
|
break;
|
|
case SSH_PENDING_CALL_AUTH_PUBKEY:
|
|
goto pending;
|
|
default:
|
|
ssh_set_error(session,
|
|
SSH_FATAL,
|
|
"Bad call during pending SSH call in ssh_userauth_try_publickey");
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/* Note, that this is intentionally before checking the signature type
|
|
* compatibility to make sure the possible EXT_INFO packet is processed,
|
|
* extensions recorded and the right signature type is used below
|
|
*/
|
|
rc = ssh_userauth_request_service(session);
|
|
if (rc == SSH_AGAIN) {
|
|
return SSH_AUTH_AGAIN;
|
|
} else if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/* Cert auth requires presenting the cert type name (*-cert@openssh.com) */
|
|
key_type = privkey->cert != NULL ? privkey->cert_type : privkey->type;
|
|
|
|
/* Check if the given public key algorithm is allowed */
|
|
sig_type_c = ssh_key_get_signature_algorithm(session, key_type);
|
|
if (sig_type_c == NULL) {
|
|
ssh_set_error(session, SSH_REQUEST_DENIED,
|
|
"Invalid key type (unknown)");
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
rc = ssh_key_algorithm_allowed(session, sig_type_c);
|
|
if (!rc) {
|
|
ssh_set_error(session, SSH_REQUEST_DENIED,
|
|
"The key algorithm '%s' is not allowed to be used by"
|
|
" PUBLICKEY_ACCEPTED_TYPES configuration option",
|
|
sig_type_c);
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
allowed = ssh_key_size_allowed(session, privkey);
|
|
if (!allowed) {
|
|
ssh_set_error(session, SSH_REQUEST_DENIED,
|
|
"The '%s' key type of size %d is not allowed by "
|
|
"RSA_MIN_SIZE", sig_type_c, ssh_key_size(privkey));
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
|
|
/* get public key or cert */
|
|
rc = ssh_pki_export_pubkey_blob(privkey, &str);
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
SSH_LOG(SSH_LOG_TRACE, "Sending signature type %s", sig_type_c);
|
|
/* request */
|
|
rc = ssh_buffer_pack(session->out_buffer, "bsssbsS",
|
|
SSH2_MSG_USERAUTH_REQUEST,
|
|
username ? username : session->opts.username,
|
|
"ssh-connection",
|
|
"publickey",
|
|
1, /* private key */
|
|
sig_type_c, /* algo */
|
|
str /* public key or cert */
|
|
);
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
SSH_STRING_FREE(str);
|
|
|
|
/* Get the hash type to be used in the signature based on the key type */
|
|
hash_type = ssh_key_type_to_hash(session, privkey->type);
|
|
|
|
/* sign the buffer with the private key */
|
|
str = ssh_pki_do_sign(session, session->out_buffer, privkey, hash_type);
|
|
if (str == NULL) {
|
|
goto fail;
|
|
}
|
|
|
|
rc = ssh_buffer_add_ssh_string(session->out_buffer, str);
|
|
SSH_STRING_FREE(str);
|
|
str = NULL;
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
session->auth.current_method = SSH_AUTH_METHOD_PUBLICKEY;
|
|
session->auth.state = SSH_AUTH_STATE_PUBKEY_AUTH_SENT;
|
|
session->pending_call_state = SSH_PENDING_CALL_AUTH_PUBKEY;
|
|
rc = ssh_packet_send(session);
|
|
if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
pending:
|
|
rc = ssh_userauth_get_response(session);
|
|
if (rc != SSH_AUTH_AGAIN) {
|
|
session->pending_call_state = SSH_PENDING_CALL_NONE;
|
|
}
|
|
|
|
return rc;
|
|
fail:
|
|
SSH_STRING_FREE(str);
|
|
ssh_set_error_oom(session);
|
|
ssh_buffer_reinit(session->out_buffer);
|
|
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
static int ssh_userauth_agent_publickey(ssh_session session,
|
|
const char *username,
|
|
ssh_key pubkey)
|
|
{
|
|
ssh_string pubkey_s = NULL;
|
|
ssh_string sig_blob = NULL;
|
|
const char *sig_type_c = NULL;
|
|
bool allowed;
|
|
int rc;
|
|
|
|
switch (session->pending_call_state) {
|
|
case SSH_PENDING_CALL_NONE:
|
|
break;
|
|
case SSH_PENDING_CALL_AUTH_AGENT:
|
|
goto pending;
|
|
default:
|
|
ssh_set_error(session,
|
|
SSH_FATAL,
|
|
"Bad call during pending SSH call in %s",
|
|
__func__);
|
|
return SSH_ERROR;
|
|
}
|
|
|
|
/* Note, that this is intentionally before checking the signature type
|
|
* compatibility to make sure the possible EXT_INFO packet is processed,
|
|
* extensions recorded and the right signature type is used below
|
|
*/
|
|
rc = ssh_userauth_request_service(session);
|
|
if (rc == SSH_AGAIN) {
|
|
return SSH_AUTH_AGAIN;
|
|
} else if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/* public key */
|
|
rc = ssh_pki_export_pubkey_blob(pubkey, &pubkey_s);
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
/* Check if the given public key algorithm is allowed */
|
|
sig_type_c = ssh_key_get_signature_algorithm(session, pubkey->type);
|
|
if (sig_type_c == NULL) {
|
|
ssh_set_error(session, SSH_REQUEST_DENIED,
|
|
"Invalid key type (unknown)");
|
|
SSH_STRING_FREE(pubkey_s);
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
rc = ssh_key_algorithm_allowed(session, sig_type_c);
|
|
if (!rc) {
|
|
ssh_set_error(session, SSH_REQUEST_DENIED,
|
|
"The key algorithm '%s' is not allowed to be used by"
|
|
" PUBLICKEY_ACCEPTED_TYPES configuration option",
|
|
sig_type_c);
|
|
SSH_STRING_FREE(pubkey_s);
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
allowed = ssh_key_size_allowed(session, pubkey);
|
|
if (!allowed) {
|
|
ssh_set_error(session, SSH_REQUEST_DENIED,
|
|
"The '%s' key type of size %d is not allowed by "
|
|
"RSA_MIN_SIZE", sig_type_c, ssh_key_size(pubkey));
|
|
SSH_STRING_FREE(pubkey_s);
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
|
|
/* request */
|
|
rc = ssh_buffer_pack(session->out_buffer, "bsssbsS",
|
|
SSH2_MSG_USERAUTH_REQUEST,
|
|
username ? username : session->opts.username,
|
|
"ssh-connection",
|
|
"publickey",
|
|
1, /* private key */
|
|
sig_type_c, /* algo */
|
|
pubkey_s /* public key */
|
|
);
|
|
SSH_STRING_FREE(pubkey_s);
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
/* sign the buffer with the private key */
|
|
sig_blob = ssh_pki_do_sign_agent(session, session->out_buffer, pubkey);
|
|
if (sig_blob == NULL) {
|
|
goto fail;
|
|
}
|
|
|
|
rc = ssh_buffer_add_ssh_string(session->out_buffer, sig_blob);
|
|
SSH_STRING_FREE(sig_blob);
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
session->auth.current_method = SSH_AUTH_METHOD_PUBLICKEY;
|
|
session->auth.state = SSH_AUTH_STATE_PUBKEY_AUTH_SENT;
|
|
session->pending_call_state = SSH_PENDING_CALL_AUTH_AGENT;
|
|
rc = ssh_packet_send(session);
|
|
if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
pending:
|
|
rc = ssh_userauth_get_response(session);
|
|
if (rc != SSH_AUTH_AGAIN) {
|
|
session->pending_call_state = SSH_PENDING_CALL_NONE;
|
|
}
|
|
|
|
return rc;
|
|
fail:
|
|
ssh_set_error_oom(session);
|
|
ssh_buffer_reinit(session->out_buffer);
|
|
SSH_STRING_FREE(pubkey_s);
|
|
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
enum ssh_agent_state_e {
|
|
SSH_AGENT_STATE_NONE = 0,
|
|
SSH_AGENT_STATE_PUBKEY,
|
|
SSH_AGENT_STATE_CERT,
|
|
SSH_AGENT_STATE_AUTH
|
|
};
|
|
|
|
struct ssh_agent_state_struct {
|
|
enum ssh_agent_state_e state;
|
|
ssh_key pubkey;
|
|
char *comment;
|
|
};
|
|
|
|
/* Internal function */
|
|
void ssh_agent_state_free(void *data)
|
|
{
|
|
struct ssh_agent_state_struct *state = data;
|
|
|
|
if (state) {
|
|
SSH_STRING_FREE_CHAR(state->comment);
|
|
ssh_key_free(state->pubkey);
|
|
free (state);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Try to do public key authentication with ssh agent.
|
|
*
|
|
* @param[in] session The ssh session to use.
|
|
*
|
|
* @param[in] username The username, this SHOULD be NULL.
|
|
*
|
|
* @return SSH_AUTH_ERROR: A serious error happened.\n
|
|
* SSH_AUTH_DENIED: The server doesn't accept that public key as an
|
|
* authentication token. Try another key or another
|
|
* method.\n
|
|
* SSH_AUTH_PARTIAL: You've been partially authenticated, you still
|
|
* have to use another method.\n
|
|
* SSH_AUTH_SUCCESS: The public key is accepted, you want now to use
|
|
* ssh_userauth_publickey().\n
|
|
* SSH_AUTH_AGAIN: In nonblocking mode, you've got to call this again
|
|
* later.
|
|
*
|
|
* @note Most server implementations do not permit changing the username during
|
|
* authentication. The username should only be set with ssh_options_set() only
|
|
* before you connect to the server.
|
|
*/
|
|
int ssh_userauth_agent(ssh_session session,
|
|
const char *username)
|
|
{
|
|
int rc = SSH_AUTH_ERROR;
|
|
struct ssh_agent_state_struct *state = NULL;
|
|
ssh_key *configKeys = NULL;
|
|
ssh_key *configCerts = NULL;
|
|
size_t configKeysCount = 0;
|
|
size_t configCertsCount = 0;
|
|
size_t i;
|
|
|
|
if (session == NULL) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
if (!ssh_agent_is_running(session)) {
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
|
|
if (!session->agent_state) {
|
|
session->agent_state = malloc(sizeof(struct ssh_agent_state_struct));
|
|
if (!session->agent_state) {
|
|
ssh_set_error_oom(session);
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
ZERO_STRUCTP(session->agent_state);
|
|
session->agent_state->state = SSH_AGENT_STATE_NONE;
|
|
}
|
|
|
|
state = session->agent_state;
|
|
if (state->pubkey == NULL) {
|
|
state->pubkey = ssh_agent_get_first_ident(session, &state->comment);
|
|
}
|
|
|
|
if (state->pubkey == NULL) {
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
|
|
if (session->opts.identities_only) {
|
|
/*
|
|
* Read keys mentioned in the config, so we can check if key from agent
|
|
* is in there.
|
|
*/
|
|
size_t identityLen = ssh_list_count(session->opts.identity);
|
|
size_t certsLen = ssh_list_count(session->opts.certificate);
|
|
struct ssh_iterator *it = ssh_list_get_iterator(session->opts.identity);
|
|
|
|
configKeys = malloc(identityLen * sizeof(ssh_key));
|
|
configCerts = malloc((certsLen + identityLen) * sizeof(ssh_key));
|
|
if (configKeys == NULL || configCerts == NULL) {
|
|
free(configKeys);
|
|
free(configCerts);
|
|
ssh_set_error_oom(session);
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
while (it != NULL && configKeysCount < identityLen) {
|
|
const char *privkeyFile = it->data;
|
|
size_t certPathLen;
|
|
char *certFile = NULL;
|
|
ssh_key pubkey = NULL;
|
|
ssh_key cert = NULL;
|
|
|
|
/*
|
|
* Read the private key file listed in the config, but we're only
|
|
* interested in the public key. Don't try to decrypt private key.
|
|
*/
|
|
rc = ssh_pki_import_pubkey_file(privkeyFile, &pubkey);
|
|
if (rc == SSH_OK) {
|
|
configKeys[configKeysCount++] = pubkey;
|
|
} else {
|
|
char *pubkeyFile = NULL;
|
|
size_t pubkeyPathLen = strlen(privkeyFile) + sizeof(".pub");
|
|
|
|
SSH_KEY_FREE(pubkey);
|
|
|
|
/*
|
|
* If we couldn't get the public key from the private key file,
|
|
* try a .pub file instead.
|
|
*/
|
|
pubkeyFile = malloc(pubkeyPathLen);
|
|
if (!pubkeyFile) {
|
|
ssh_set_error_oom(session);
|
|
rc = SSH_AUTH_ERROR;
|
|
goto done;
|
|
}
|
|
snprintf(pubkeyFile, pubkeyPathLen, "%s.pub", privkeyFile);
|
|
rc = ssh_pki_import_pubkey_file(pubkeyFile, &pubkey);
|
|
free(pubkeyFile);
|
|
if (rc == SSH_OK) {
|
|
configKeys[configKeysCount++] = pubkey;
|
|
} else if (pubkey) {
|
|
SSH_KEY_FREE(pubkey);
|
|
}
|
|
}
|
|
/* Now try to see if there is a certificate with default name
|
|
* do not merge it yet with the key as we need to try first the
|
|
* non-certified key */
|
|
certPathLen = strlen(privkeyFile) + sizeof("-cert.pub");
|
|
certFile = malloc(certPathLen);
|
|
if (!certFile) {
|
|
ssh_set_error_oom(session);
|
|
rc = SSH_AUTH_ERROR;
|
|
goto done;
|
|
}
|
|
snprintf(certFile, certPathLen, "%s-cert.pub", privkeyFile);
|
|
rc = ssh_pki_import_cert_file(certFile, &cert);
|
|
free(certFile);
|
|
if (rc == SSH_OK) {
|
|
configCerts[configCertsCount++] = cert;
|
|
} else if (cert) {
|
|
SSH_KEY_FREE(cert);
|
|
}
|
|
|
|
it = it->next;
|
|
}
|
|
/* And now load separately-listed certificates. */
|
|
it = ssh_list_get_iterator(session->opts.certificate);
|
|
while (it != NULL && configCertsCount < certsLen + identityLen) {
|
|
const char *certFile = it->data;
|
|
ssh_key cert = NULL;
|
|
|
|
rc = ssh_pki_import_cert_file(certFile, &cert);
|
|
if (rc == SSH_OK) {
|
|
configCerts[configCertsCount++] = cert;
|
|
} else if (cert) {
|
|
SSH_KEY_FREE(cert);
|
|
}
|
|
|
|
it = it->next;
|
|
}
|
|
}
|
|
|
|
while (state->pubkey != NULL) {
|
|
if (state->state == SSH_AGENT_STATE_NONE) {
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Trying identity %s",
|
|
state->comment);
|
|
if (session->opts.identities_only) {
|
|
/* Check if this key is one of the keys listed in the config */
|
|
bool found_key = false;
|
|
for (i = 0; i < configKeysCount; i++) {
|
|
int cmp = ssh_key_cmp(state->pubkey,
|
|
configKeys[i],
|
|
SSH_KEY_CMP_PUBLIC);
|
|
if (cmp == 0) {
|
|
found_key = true;
|
|
break;
|
|
}
|
|
}
|
|
/* or in separate certificates */
|
|
for (i = 0; i < configCertsCount; i++) {
|
|
int cmp = ssh_key_cmp(state->pubkey,
|
|
configCerts[i],
|
|
SSH_KEY_CMP_PUBLIC);
|
|
if (cmp == 0) {
|
|
found_key = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found_key) {
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Identities only is enabled and identity %s was "
|
|
"not listed in config, skipping",
|
|
state->comment);
|
|
SSH_STRING_FREE_CHAR(state->comment);
|
|
state->comment = NULL;
|
|
SSH_KEY_FREE(state->pubkey);
|
|
state->pubkey = ssh_agent_get_next_ident(
|
|
session, &state->comment);
|
|
|
|
if (state->pubkey == NULL) {
|
|
rc = SSH_AUTH_DENIED;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
if (state->state == SSH_AGENT_STATE_NONE ||
|
|
state->state == SSH_AGENT_STATE_PUBKEY ||
|
|
state->state == SSH_AGENT_STATE_CERT) {
|
|
rc = ssh_userauth_try_publickey(session, username, state->pubkey);
|
|
if (rc == SSH_AUTH_ERROR) {
|
|
ssh_agent_state_free(state);
|
|
session->agent_state = NULL;
|
|
goto done;
|
|
} else if (rc == SSH_AUTH_AGAIN) {
|
|
state->state = (state->state == SSH_AGENT_STATE_NONE ?
|
|
SSH_AGENT_STATE_PUBKEY : state->state);
|
|
goto done;
|
|
} else if (rc != SSH_AUTH_SUCCESS) {
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Public key of %s refused by server",
|
|
state->comment);
|
|
if (state->state == SSH_AGENT_STATE_PUBKEY) {
|
|
for (i = 0; i < configCertsCount; i++) {
|
|
int cmp = ssh_key_cmp(state->pubkey,
|
|
configCerts[i],
|
|
SSH_KEY_CMP_PUBLIC);
|
|
if (cmp == 0) {
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Retry with matching certificate");
|
|
SSH_KEY_FREE(state->pubkey);
|
|
state->pubkey = ssh_key_dup(configCerts[i]);
|
|
state->state = SSH_AGENT_STATE_CERT;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
SSH_STRING_FREE_CHAR(state->comment);
|
|
state->comment = NULL;
|
|
SSH_KEY_FREE(state->pubkey);
|
|
state->pubkey = ssh_agent_get_next_ident(session,
|
|
&state->comment);
|
|
state->state = SSH_AGENT_STATE_NONE;
|
|
continue;
|
|
}
|
|
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Public key of %s accepted by server",
|
|
state->comment);
|
|
state->state = SSH_AGENT_STATE_AUTH;
|
|
}
|
|
if (state->state == SSH_AGENT_STATE_AUTH) {
|
|
rc = ssh_userauth_agent_publickey(session, username, state->pubkey);
|
|
if (rc == SSH_AUTH_AGAIN) {
|
|
goto done;
|
|
}
|
|
SSH_STRING_FREE_CHAR(state->comment);
|
|
state->comment = NULL;
|
|
if (rc == SSH_AUTH_ERROR || rc == SSH_AUTH_PARTIAL) {
|
|
ssh_agent_state_free(session->agent_state);
|
|
session->agent_state = NULL;
|
|
goto done;
|
|
} else if (rc != SSH_AUTH_SUCCESS) {
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Server accepted public key but refused the signature");
|
|
SSH_KEY_FREE(state->pubkey);
|
|
state->pubkey = ssh_agent_get_next_ident(session,
|
|
&state->comment);
|
|
state->state = SSH_AGENT_STATE_NONE;
|
|
continue;
|
|
}
|
|
ssh_agent_state_free (session->agent_state);
|
|
session->agent_state = NULL;
|
|
rc = SSH_AUTH_SUCCESS;
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
ssh_agent_state_free (session->agent_state);
|
|
session->agent_state = NULL;
|
|
done:
|
|
for (i = 0; i < configKeysCount; i++) {
|
|
ssh_key_free(configKeys[i]);
|
|
}
|
|
free(configKeys);
|
|
for (i = 0; i < configCertsCount; i++) {
|
|
ssh_key_free(configCerts[i]);
|
|
}
|
|
free(configCerts);
|
|
return rc;
|
|
}
|
|
|
|
enum ssh_auth_auto_state_e {
|
|
SSH_AUTH_AUTO_STATE_NONE = 0,
|
|
SSH_AUTH_AUTO_STATE_PUBKEY,
|
|
SSH_AUTH_AUTO_STATE_KEY_IMPORTED,
|
|
SSH_AUTH_AUTO_STATE_CERTIFICATE_FILE,
|
|
SSH_AUTH_AUTO_STATE_CERTIFICATE_OPTION,
|
|
SSH_AUTH_AUTO_STATE_PUBKEY_ACCEPTED
|
|
};
|
|
|
|
struct ssh_auth_auto_state_struct {
|
|
enum ssh_auth_auto_state_e state;
|
|
struct ssh_iterator *it;
|
|
ssh_key privkey;
|
|
ssh_key pubkey;
|
|
ssh_key cert;
|
|
struct ssh_iterator *cert_it;
|
|
};
|
|
|
|
/**
|
|
* @brief Get the identity that is currently being processed by
|
|
* ssh_userauth_publickey_auto()
|
|
*
|
|
* This is meant to be used by a callback that happens as part of the
|
|
* execution of ssh_userauth_publickey_auto(). The auth_function
|
|
* callback might want to know which key a passphrase is needed for,
|
|
* for example.
|
|
*
|
|
* @param[in] session The SSH session.
|
|
*
|
|
* @param[out] value The value to get into. As a char**, space will be
|
|
* allocated by the function for the value, it is
|
|
* your responsibility to free the memory using
|
|
* ssh_string_free_char().
|
|
*
|
|
* @return SSH_OK on success, SSH_ERROR on error.
|
|
*/
|
|
int ssh_userauth_publickey_auto_get_current_identity(ssh_session session,
|
|
char** value)
|
|
{
|
|
const char *id = NULL;
|
|
|
|
if (session == NULL) {
|
|
return SSH_ERROR;
|
|
}
|
|
|
|
if (value == NULL) {
|
|
ssh_set_error_invalid(session);
|
|
return SSH_ERROR;
|
|
}
|
|
|
|
if (session->auth.auto_state != NULL &&
|
|
session->auth.auto_state->it != NULL) {
|
|
id = session->auth.auto_state->it->data;
|
|
}
|
|
|
|
if (id == NULL) {
|
|
return SSH_ERROR;
|
|
}
|
|
|
|
*value = strdup(id);
|
|
if (*value == NULL) {
|
|
ssh_set_error_oom(session);
|
|
return SSH_ERROR;
|
|
}
|
|
|
|
return SSH_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Tries to automatically authenticate with public key and "none"
|
|
*
|
|
* It may fail, for instance it doesn't ask for a password and uses a default
|
|
* asker for passphrases (in case the private key is encrypted).
|
|
*
|
|
* @param[in] session The SSH session.
|
|
*
|
|
* @param[in] username The username, this SHOULD be NULL.
|
|
*
|
|
* @param[in] passphrase Use this passphrase to unlock the privatekey. Use NULL
|
|
* if you don't want to use a passphrase or the user
|
|
* should be asked.
|
|
*
|
|
* @return SSH_AUTH_ERROR: A serious error happened.\n
|
|
* SSH_AUTH_DENIED: The server doesn't accept that public key as an
|
|
* authentication token. Try another key or another
|
|
* method.\n
|
|
* SSH_AUTH_PARTIAL: You've been partially authenticated, you still
|
|
* have to use another method.\n
|
|
* SSH_AUTH_SUCCESS: Authentication success\n
|
|
* SSH_AUTH_AGAIN: In nonblocking mode, you've got to call this again
|
|
* later.
|
|
*
|
|
* @note Most server implementations do not permit changing the username during
|
|
* authentication. The username should only be set with ssh_options_set() only
|
|
* before you connect to the server.
|
|
*
|
|
* The OpenSSH iterates over the identities and first try the plain public key
|
|
* and then the certificate if it is in place.
|
|
*/
|
|
int ssh_userauth_publickey_auto(ssh_session session,
|
|
const char *username,
|
|
const char *passphrase)
|
|
{
|
|
ssh_auth_callback auth_fn = NULL;
|
|
void *auth_data = NULL;
|
|
struct ssh_auth_auto_state_struct *state = NULL;
|
|
int rc;
|
|
|
|
if (session == NULL) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
if (! (session->opts.flags & SSH_OPT_FLAG_PUBKEY_AUTH)) {
|
|
session->auth.supported_methods &= ~SSH_AUTH_METHOD_PUBLICKEY;
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
if (session->common.callbacks) {
|
|
auth_fn = session->common.callbacks->auth_function;
|
|
auth_data = session->common.callbacks->userdata;
|
|
}
|
|
if (!session->auth.auto_state) {
|
|
session->auth.auto_state =
|
|
calloc(1, sizeof(struct ssh_auth_auto_state_struct));
|
|
if (!session->auth.auto_state) {
|
|
ssh_set_error_oom(session);
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/* Set state explicitly */
|
|
session->auth.auto_state->state = SSH_AUTH_AUTO_STATE_NONE;
|
|
}
|
|
state = session->auth.auto_state;
|
|
if (state->state == SSH_AUTH_AUTO_STATE_NONE) {
|
|
/* Try authentication with ssh-agent first */
|
|
rc = ssh_userauth_agent(session, username);
|
|
if (rc == SSH_AUTH_SUCCESS ||
|
|
rc == SSH_AUTH_PARTIAL ||
|
|
rc == SSH_AUTH_AGAIN) {
|
|
return rc;
|
|
}
|
|
state->state = SSH_AUTH_AUTO_STATE_PUBKEY;
|
|
}
|
|
if (state->it == NULL) {
|
|
state->it = ssh_list_get_iterator(session->opts.identity);
|
|
}
|
|
|
|
while (state->it != NULL) {
|
|
const char *privkey_file = state->it->data;
|
|
char pubkey_file[PATH_MAX] = {0};
|
|
|
|
if (state->state == SSH_AUTH_AUTO_STATE_PUBKEY) {
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Trying to authenticate with %s",
|
|
privkey_file);
|
|
state->cert = NULL;
|
|
state->privkey = NULL;
|
|
state->pubkey = NULL;
|
|
|
|
#ifdef WITH_PKCS11_URI
|
|
if (ssh_pki_is_uri(privkey_file)) {
|
|
char *pub_uri_from_priv = NULL;
|
|
SSH_LOG(SSH_LOG_INFO,
|
|
"Authenticating with PKCS #11 URI.");
|
|
pub_uri_from_priv = ssh_pki_export_pub_uri_from_priv_uri(privkey_file);
|
|
if (pub_uri_from_priv == NULL) {
|
|
return SSH_ERROR;
|
|
} else {
|
|
snprintf(pubkey_file,
|
|
sizeof(pubkey_file),
|
|
"%s",
|
|
pub_uri_from_priv);
|
|
SAFE_FREE(pub_uri_from_priv);
|
|
}
|
|
} else
|
|
#endif /* WITH_PKCS11_URI */
|
|
{
|
|
snprintf(pubkey_file,
|
|
sizeof(pubkey_file),
|
|
"%s.pub",
|
|
privkey_file);
|
|
}
|
|
|
|
rc = ssh_pki_import_pubkey_file(pubkey_file, &state->pubkey);
|
|
if (rc == SSH_ERROR) {
|
|
ssh_set_error(session,
|
|
SSH_FATAL,
|
|
"Failed to import public key: %s",
|
|
pubkey_file);
|
|
SAFE_FREE(session->auth.auto_state);
|
|
return SSH_AUTH_ERROR;
|
|
} else if (rc == SSH_EOF) {
|
|
/* Read the private key and save the public key to file */
|
|
rc = ssh_pki_import_privkey_file(privkey_file,
|
|
passphrase,
|
|
auth_fn,
|
|
auth_data,
|
|
&state->privkey);
|
|
if (rc == SSH_ERROR) {
|
|
ssh_set_error(session,
|
|
SSH_FATAL,
|
|
"Failed to read private key: %s",
|
|
privkey_file);
|
|
state->it = state->it->next;
|
|
continue;
|
|
} else if (rc == SSH_EOF) {
|
|
/* If the file doesn't exist, continue */
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Private key %s doesn't exist.",
|
|
privkey_file);
|
|
state->it = state->it->next;
|
|
continue;
|
|
}
|
|
|
|
rc = ssh_pki_export_privkey_to_pubkey(state->privkey,
|
|
&state->pubkey);
|
|
if (rc == SSH_ERROR) {
|
|
SSH_KEY_FREE(state->privkey);
|
|
SAFE_FREE(session->auth.auto_state);
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
rc = ssh_pki_export_pubkey_file(state->pubkey, pubkey_file);
|
|
if (rc == SSH_ERROR) {
|
|
SSH_LOG(SSH_LOG_TRACE,
|
|
"Could not write public key to file: %s",
|
|
pubkey_file);
|
|
}
|
|
}
|
|
state->state = SSH_AUTH_AUTO_STATE_KEY_IMPORTED;
|
|
}
|
|
if (state->state == SSH_AUTH_AUTO_STATE_KEY_IMPORTED ||
|
|
state->state == SSH_AUTH_AUTO_STATE_CERTIFICATE_FILE ||
|
|
state->state == SSH_AUTH_AUTO_STATE_CERTIFICATE_OPTION) {
|
|
ssh_key k = state->pubkey;
|
|
if (state->state != SSH_AUTH_AUTO_STATE_KEY_IMPORTED) {
|
|
k = state->cert;
|
|
}
|
|
rc = ssh_userauth_try_publickey(session, username, k);
|
|
if (rc == SSH_AUTH_ERROR) {
|
|
SSH_LOG(SSH_LOG_TRACE,
|
|
"Public key authentication error for %s",
|
|
privkey_file);
|
|
SSH_KEY_FREE(state->cert);
|
|
SSH_KEY_FREE(state->privkey);
|
|
SSH_KEY_FREE(state->pubkey);
|
|
SAFE_FREE(session->auth.auto_state);
|
|
return rc;
|
|
} else if (rc == SSH_AUTH_AGAIN) {
|
|
return rc;
|
|
} else if (rc != SSH_AUTH_SUCCESS) {
|
|
int r; /* do not reuse `rc` as it is used to return from here */
|
|
SSH_KEY_FREE(state->cert);
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Public key for %s%s refused by server",
|
|
privkey_file,
|
|
(state->state != SSH_AUTH_AUTO_STATE_KEY_IMPORTED
|
|
? " (with certificate)" : ""));
|
|
/* Try certificate file by appending -cert.pub (if present) */
|
|
if (state->state == SSH_AUTH_AUTO_STATE_KEY_IMPORTED) {
|
|
char cert_file[PATH_MAX] = {0};
|
|
ssh_key cert = NULL;
|
|
|
|
snprintf(cert_file,
|
|
sizeof(cert_file),
|
|
"%s-cert.pub",
|
|
privkey_file);
|
|
SSH_LOG(SSH_LOG_TRACE,
|
|
"Trying to load the certificate %s (default path)",
|
|
cert_file);
|
|
r = ssh_pki_import_cert_file(cert_file, &cert);
|
|
if (r == SSH_OK) {
|
|
/* TODO check the pubkey and certs match */
|
|
SSH_LOG(SSH_LOG_TRACE,
|
|
"Certificate loaded %s. Retry the authentication.",
|
|
cert_file);
|
|
state->state = SSH_AUTH_AUTO_STATE_CERTIFICATE_FILE;
|
|
SSH_KEY_FREE(state->cert);
|
|
state->cert = cert;
|
|
/* try to authenticate with this certificate */
|
|
continue;
|
|
}
|
|
/* if the file does not exists, try configuration options */
|
|
state->state = SSH_AUTH_AUTO_STATE_CERTIFICATE_OPTION;
|
|
}
|
|
/* Try certificate files loaded through options */
|
|
if (state->state == SSH_AUTH_AUTO_STATE_CERTIFICATE_OPTION) {
|
|
SSH_KEY_FREE(state->cert);
|
|
if (state->cert_it == NULL) {
|
|
state->cert_it = ssh_list_get_iterator(session->opts.certificate);
|
|
}
|
|
while (state->cert_it != NULL) {
|
|
const char *cert_file = state->cert_it->data;
|
|
ssh_key cert = NULL;
|
|
|
|
SSH_LOG(SSH_LOG_TRACE,
|
|
"Trying to load the certificate %s (options)",
|
|
cert_file);
|
|
r = ssh_pki_import_cert_file(cert_file, &cert);
|
|
if (r == SSH_OK) {
|
|
int cmp = ssh_key_cmp(cert,
|
|
state->pubkey,
|
|
SSH_KEY_CMP_PUBLIC);
|
|
if (cmp != 0) {
|
|
state->cert_it = state->cert_it->next;
|
|
SSH_KEY_FREE(cert);
|
|
continue; /* with next cert */
|
|
}
|
|
SSH_LOG(SSH_LOG_TRACE,
|
|
"Found matching certificate %s in options. Retry the authentication.",
|
|
cert_file);
|
|
state->cert = cert;
|
|
cert = NULL;
|
|
state->state = SSH_AUTH_AUTO_STATE_CERTIFICATE_OPTION;
|
|
/* try to authenticate with this identity */
|
|
break; /* try this cert */
|
|
}
|
|
/* continue with next identity */
|
|
}
|
|
if (state->cert != NULL) {
|
|
continue; /* retry with the certificate */
|
|
}
|
|
}
|
|
SSH_KEY_FREE(state->cert);
|
|
SSH_KEY_FREE(state->privkey);
|
|
SSH_KEY_FREE(state->pubkey);
|
|
state->it = state->it->next;
|
|
state->state = SSH_AUTH_AUTO_STATE_PUBKEY;
|
|
continue;
|
|
}
|
|
state->state = SSH_AUTH_AUTO_STATE_PUBKEY_ACCEPTED;
|
|
}
|
|
if (state->state == SSH_AUTH_AUTO_STATE_PUBKEY_ACCEPTED) {
|
|
/* Public key has been accepted by the server */
|
|
if (state->privkey == NULL) {
|
|
rc = ssh_pki_import_privkey_file(privkey_file,
|
|
passphrase,
|
|
auth_fn,
|
|
auth_data,
|
|
&state->privkey);
|
|
if (rc == SSH_ERROR) {
|
|
SSH_KEY_FREE(state->cert);
|
|
SSH_KEY_FREE(state->pubkey);
|
|
ssh_set_error(session,
|
|
SSH_FATAL,
|
|
"Failed to read private key: %s",
|
|
privkey_file);
|
|
state->it = state->it->next;
|
|
state->state = SSH_AUTH_AUTO_STATE_PUBKEY;
|
|
continue;
|
|
} else if (rc == SSH_EOF) {
|
|
/* If the file doesn't exist, continue */
|
|
SSH_KEY_FREE(state->cert);
|
|
SSH_KEY_FREE(state->pubkey);
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Private key %s doesn't exist.",
|
|
privkey_file);
|
|
state->it = state->it->next;
|
|
state->state = SSH_AUTH_AUTO_STATE_PUBKEY;
|
|
continue;
|
|
}
|
|
}
|
|
if (state->cert != NULL && !is_cert_type(state->privkey->cert_type)) {
|
|
rc = ssh_pki_copy_cert_to_privkey(state->cert, state->privkey);
|
|
if (rc != SSH_OK) {
|
|
SSH_KEY_FREE(state->cert);
|
|
SSH_KEY_FREE(state->privkey);
|
|
SSH_KEY_FREE(state->pubkey);
|
|
ssh_set_error(session,
|
|
SSH_FATAL,
|
|
"Failed to copy cert to private key");
|
|
state->it = state->it->next;
|
|
state->state = SSH_AUTH_AUTO_STATE_PUBKEY;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
rc = ssh_userauth_publickey(session, username, state->privkey);
|
|
if (rc != SSH_AUTH_AGAIN && rc != SSH_AUTH_DENIED) {
|
|
bool cert_used = (state->cert != NULL);
|
|
SSH_KEY_FREE(state->cert);
|
|
SSH_KEY_FREE(state->privkey);
|
|
SSH_KEY_FREE(state->pubkey);
|
|
SAFE_FREE(session->auth.auto_state);
|
|
if (rc == SSH_AUTH_SUCCESS) {
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Successfully authenticated using %s%s",
|
|
privkey_file,
|
|
(cert_used ? " and certificate" : ""));
|
|
}
|
|
return rc;
|
|
}
|
|
if (rc == SSH_AUTH_AGAIN) {
|
|
return rc;
|
|
}
|
|
|
|
SSH_KEY_FREE(state->cert);
|
|
SSH_KEY_FREE(state->privkey);
|
|
SSH_KEY_FREE(state->pubkey);
|
|
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"The server accepted the public key but refused the signature");
|
|
state->it = state->it->next;
|
|
state->state = SSH_AUTH_AUTO_STATE_PUBKEY;
|
|
/* continue */
|
|
}
|
|
}
|
|
SSH_LOG(SSH_LOG_WARN,
|
|
"Access denied: Tried every public key, none matched");
|
|
SAFE_FREE(session->auth.auto_state);
|
|
return SSH_AUTH_DENIED;
|
|
}
|
|
|
|
/**
|
|
* @brief Try to authenticate by password.
|
|
*
|
|
* This authentication method is normally disabled on SSHv2 server. You should
|
|
* use keyboard-interactive mode.
|
|
*
|
|
* The 'password' value MUST be encoded UTF-8. It is up to the server how to
|
|
* interpret the password and validate it against the password database.
|
|
* However, if you read the password in some other encoding, you MUST convert
|
|
* the password to UTF-8.
|
|
*
|
|
* @param[in] session The ssh session to use.
|
|
*
|
|
* @param[in] username The username, this SHOULD be NULL.
|
|
*
|
|
* @param[in] password The password to authenticate in UTF-8.
|
|
*
|
|
* @returns SSH_AUTH_ERROR: A serious error happened.\n
|
|
* SSH_AUTH_DENIED: Authentication failed: use another method\n
|
|
* SSH_AUTH_PARTIAL: You've been partially authenticated, you still
|
|
* have to use another method\n
|
|
* SSH_AUTH_SUCCESS: Authentication success\n
|
|
* SSH_AUTH_AGAIN: In nonblocking mode, you've got to call this again
|
|
* later.
|
|
*
|
|
* @note Most server implementations do not permit changing the username during
|
|
* authentication. The username should only be set with ssh_options_set() only
|
|
* before you connect to the server.
|
|
*
|
|
* @see ssh_userauth_none()
|
|
* @see ssh_userauth_kbdint()
|
|
*/
|
|
int ssh_userauth_password(ssh_session session,
|
|
const char *username,
|
|
const char *password)
|
|
{
|
|
int rc;
|
|
|
|
switch (session->pending_call_state) {
|
|
case SSH_PENDING_CALL_NONE:
|
|
break;
|
|
case SSH_PENDING_CALL_AUTH_PASSWORD:
|
|
goto pending;
|
|
default:
|
|
ssh_set_error(session,
|
|
SSH_FATAL,
|
|
"Wrong state (%d) during pending SSH call",
|
|
session->pending_call_state);
|
|
return SSH_ERROR;
|
|
}
|
|
|
|
rc = ssh_userauth_request_service(session);
|
|
if (rc == SSH_AGAIN) {
|
|
return SSH_AUTH_AGAIN;
|
|
} else if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/* request */
|
|
rc = ssh_buffer_pack(session->out_buffer, "bsssbs",
|
|
SSH2_MSG_USERAUTH_REQUEST,
|
|
username ? username : session->opts.username,
|
|
"ssh-connection",
|
|
"password",
|
|
0, /* false */
|
|
password
|
|
);
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
/* Set the buffer as secure to be explicitly zeroed when freed */
|
|
ssh_buffer_set_secure(session->out_buffer);
|
|
|
|
session->auth.current_method = SSH_AUTH_METHOD_PASSWORD;
|
|
session->auth.state = SSH_AUTH_STATE_PASSWORD_AUTH_SENT;
|
|
session->pending_call_state = SSH_PENDING_CALL_AUTH_PASSWORD;
|
|
rc = ssh_packet_send(session);
|
|
if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
pending:
|
|
rc = ssh_userauth_get_response(session);
|
|
if (rc != SSH_AUTH_AGAIN) {
|
|
session->pending_call_state = SSH_PENDING_CALL_NONE;
|
|
}
|
|
|
|
return rc;
|
|
fail:
|
|
ssh_set_error_oom(session);
|
|
ssh_buffer_reinit(session->out_buffer);
|
|
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/* LEGACY */
|
|
int ssh_userauth_agent_pubkey(ssh_session session,
|
|
const char *username,
|
|
ssh_public_key publickey)
|
|
{
|
|
ssh_key key;
|
|
int rc;
|
|
|
|
key = ssh_key_new();
|
|
if (key == NULL) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
key->type = publickey->type;
|
|
key->type_c = ssh_key_type_to_char(key->type);
|
|
key->flags = SSH_KEY_FLAG_PUBLIC;
|
|
#if defined(HAVE_LIBMBEDCRYPTO)
|
|
key->pk = publickey->rsa_pub;
|
|
#elif defined(HAVE_LIBCRYPTO)
|
|
key->key = publickey->key_pub;
|
|
#else
|
|
key->rsa = publickey->rsa_pub;
|
|
#endif /* HAVE_LIBCRYPTO */
|
|
|
|
rc = ssh_userauth_agent_publickey(session, username, key);
|
|
|
|
#if defined(HAVE_LIBMBEDCRYPTO)
|
|
key->pk = NULL;
|
|
#elif defined(HAVE_LIBCRYPTO)
|
|
key->key = NULL;
|
|
#else
|
|
key->rsa = NULL;
|
|
#endif /* HAVE_LIBCRYPTO */
|
|
ssh_key_free(key);
|
|
|
|
return rc;
|
|
}
|
|
|
|
ssh_kbdint ssh_kbdint_new(void)
|
|
{
|
|
ssh_kbdint kbd;
|
|
|
|
kbd = calloc(1, sizeof(struct ssh_kbdint_struct));
|
|
if (kbd == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
return kbd;
|
|
}
|
|
|
|
|
|
void ssh_kbdint_free(ssh_kbdint kbd)
|
|
{
|
|
size_t i, n;
|
|
|
|
if (kbd == NULL) {
|
|
return;
|
|
}
|
|
|
|
SAFE_FREE(kbd->name);
|
|
SAFE_FREE(kbd->instruction);
|
|
SAFE_FREE(kbd->echo);
|
|
|
|
n = kbd->nprompts;
|
|
if (kbd->prompts) {
|
|
for (i = 0; i < n; i++) {
|
|
if (kbd->prompts[i] != NULL) {
|
|
explicit_bzero(kbd->prompts[i], strlen(kbd->prompts[i]));
|
|
}
|
|
SAFE_FREE(kbd->prompts[i]);
|
|
}
|
|
SAFE_FREE(kbd->prompts);
|
|
}
|
|
|
|
n = kbd->nanswers;
|
|
if (kbd->answers) {
|
|
for (i = 0; i < n; i++) {
|
|
if (kbd->answers[i] != NULL) {
|
|
explicit_bzero(kbd->answers[i], strlen(kbd->answers[i]));
|
|
}
|
|
SAFE_FREE(kbd->answers[i]);
|
|
}
|
|
SAFE_FREE(kbd->answers);
|
|
}
|
|
|
|
SAFE_FREE(kbd);
|
|
}
|
|
|
|
void ssh_kbdint_clean(ssh_kbdint kbd)
|
|
{
|
|
size_t i, n;
|
|
|
|
if (kbd == NULL) {
|
|
return;
|
|
}
|
|
|
|
SAFE_FREE(kbd->name);
|
|
SAFE_FREE(kbd->instruction);
|
|
SAFE_FREE(kbd->echo);
|
|
|
|
n = kbd->nprompts;
|
|
if (kbd->prompts) {
|
|
for (i = 0; i < n; i++) {
|
|
explicit_bzero(kbd->prompts[i], strlen(kbd->prompts[i]));
|
|
SAFE_FREE(kbd->prompts[i]);
|
|
}
|
|
SAFE_FREE(kbd->prompts);
|
|
}
|
|
|
|
n = kbd->nanswers;
|
|
|
|
if (kbd->answers) {
|
|
for (i = 0; i < n; i++) {
|
|
explicit_bzero(kbd->answers[i], strlen(kbd->answers[i]));
|
|
SAFE_FREE(kbd->answers[i]);
|
|
}
|
|
SAFE_FREE(kbd->answers);
|
|
}
|
|
|
|
kbd->nprompts = 0;
|
|
kbd->nanswers = 0;
|
|
}
|
|
|
|
/*
|
|
* This function sends the first packet as explained in RFC 3066 section 3.1.
|
|
*/
|
|
static int ssh_userauth_kbdint_init(ssh_session session,
|
|
const char *username,
|
|
const char *submethods)
|
|
{
|
|
int rc;
|
|
|
|
if (session->pending_call_state == SSH_PENDING_CALL_AUTH_KBDINT_INIT) {
|
|
goto pending;
|
|
}
|
|
if (session->pending_call_state != SSH_PENDING_CALL_NONE) {
|
|
ssh_set_error_invalid(session);
|
|
return SSH_ERROR;
|
|
}
|
|
|
|
rc = ssh_userauth_request_service(session);
|
|
if (rc == SSH_AGAIN) {
|
|
return SSH_AUTH_AGAIN;
|
|
}
|
|
if (rc != SSH_OK) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/* request */
|
|
rc = ssh_buffer_pack(session->out_buffer, "bsssss",
|
|
SSH2_MSG_USERAUTH_REQUEST,
|
|
username ? username : session->opts.username,
|
|
"ssh-connection",
|
|
"keyboard-interactive",
|
|
"", /* lang (ignore it) */
|
|
submethods ? submethods : ""
|
|
);
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
|
|
session->auth.state = SSH_AUTH_STATE_KBDINT_SENT;
|
|
session->pending_call_state = SSH_PENDING_CALL_AUTH_KBDINT_INIT;
|
|
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Sending keyboard-interactive init request");
|
|
|
|
rc = ssh_packet_send(session);
|
|
if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
pending:
|
|
rc = ssh_userauth_get_response(session);
|
|
if (rc != SSH_AUTH_AGAIN)
|
|
session->pending_call_state = SSH_PENDING_CALL_NONE;
|
|
return rc;
|
|
fail:
|
|
ssh_set_error_oom(session);
|
|
ssh_buffer_reinit(session->out_buffer);
|
|
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* @brief Send the current challenge response and wait for a reply from the
|
|
* server.
|
|
*
|
|
* @returns SSH_AUTH_INFO if more info is needed
|
|
* @returns SSH_AUTH_SUCCESS
|
|
* @returns SSH_AUTH_FAILURE
|
|
* @returns SSH_AUTH_PARTIAL
|
|
*/
|
|
static int ssh_userauth_kbdint_send(ssh_session session)
|
|
{
|
|
uint32_t i;
|
|
int rc;
|
|
if (session->pending_call_state == SSH_PENDING_CALL_AUTH_KBDINT_SEND)
|
|
goto pending;
|
|
if (session->pending_call_state != SSH_PENDING_CALL_NONE) {
|
|
ssh_set_error_invalid(session);
|
|
return SSH_ERROR;
|
|
}
|
|
rc = ssh_buffer_pack(session->out_buffer, "bd",
|
|
SSH2_MSG_USERAUTH_INFO_RESPONSE,
|
|
session->kbdint->nprompts);
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
for (i = 0; i < session->kbdint->nprompts; i++) {
|
|
rc = ssh_buffer_pack(session->out_buffer, "s",
|
|
session->kbdint->answers && session->kbdint->answers[i] ?
|
|
session->kbdint->answers[i]:"");
|
|
if (rc < 0) {
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
session->auth.current_method = SSH_AUTH_METHOD_INTERACTIVE;
|
|
session->auth.state = SSH_AUTH_STATE_KBDINT_SENT;
|
|
session->pending_call_state = SSH_PENDING_CALL_AUTH_KBDINT_SEND;
|
|
ssh_kbdint_free(session->kbdint);
|
|
session->kbdint = NULL;
|
|
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"Sending keyboard-interactive response packet");
|
|
|
|
rc = ssh_packet_send(session);
|
|
if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
pending:
|
|
rc = ssh_userauth_get_response(session);
|
|
if (rc != SSH_AUTH_AGAIN)
|
|
session->pending_call_state = SSH_PENDING_CALL_NONE;
|
|
return rc;
|
|
fail:
|
|
ssh_set_error_oom(session);
|
|
ssh_buffer_reinit(session->out_buffer);
|
|
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* @brief handles a SSH_USERAUTH_INFO_REQUEST packet, as used in
|
|
* keyboard-interactive authentication, and changes the
|
|
* authentication state.
|
|
*/
|
|
SSH_PACKET_CALLBACK(ssh_packet_userauth_info_request) {
|
|
ssh_string tmp = NULL;
|
|
uint32_t nprompts;
|
|
uint32_t i;
|
|
int rc;
|
|
(void)user;
|
|
(void)type;
|
|
|
|
|
|
if (session->kbdint == NULL) {
|
|
session->kbdint = ssh_kbdint_new();
|
|
if (session->kbdint == NULL) {
|
|
ssh_set_error_oom(session);
|
|
return SSH_PACKET_USED;
|
|
}
|
|
} else {
|
|
ssh_kbdint_clean(session->kbdint);
|
|
}
|
|
|
|
rc = ssh_buffer_unpack(packet, "ssSd",
|
|
&session->kbdint->name, /* name of the "asking" window shown to client */
|
|
&session->kbdint->instruction,
|
|
&tmp, /* to ignore */
|
|
&nprompts
|
|
);
|
|
|
|
/* We don't care about tmp */
|
|
SSH_STRING_FREE(tmp);
|
|
|
|
if (rc != SSH_OK) {
|
|
ssh_set_error(session, SSH_FATAL, "Invalid USERAUTH_INFO_REQUEST msg");
|
|
ssh_kbdint_free(session->kbdint);
|
|
session->kbdint = NULL;
|
|
return SSH_PACKET_USED;
|
|
}
|
|
|
|
SSH_LOG(SSH_LOG_DEBUG,
|
|
"%" PRIu32 " keyboard-interactive prompts", nprompts);
|
|
if (nprompts > KBDINT_MAX_PROMPT) {
|
|
ssh_set_error(session, SSH_FATAL,
|
|
"Too much prompts requested by the server: %" PRIu32 " (0x%.4" PRIx32 ")",
|
|
nprompts, nprompts);
|
|
ssh_kbdint_free(session->kbdint);
|
|
session->kbdint = NULL;
|
|
|
|
return SSH_PACKET_USED;
|
|
}
|
|
|
|
session->kbdint->nprompts = nprompts;
|
|
session->kbdint->nanswers = nprompts;
|
|
session->kbdint->prompts = calloc(nprompts, sizeof(char *));
|
|
if (session->kbdint->prompts == NULL) {
|
|
session->kbdint->nprompts = 0;
|
|
ssh_set_error_oom(session);
|
|
ssh_kbdint_free(session->kbdint);
|
|
session->kbdint = NULL;
|
|
|
|
return SSH_PACKET_USED;
|
|
}
|
|
|
|
session->kbdint->echo = calloc(nprompts, sizeof(unsigned char));
|
|
if (session->kbdint->echo == NULL) {
|
|
session->kbdint->nprompts = 0;
|
|
ssh_set_error_oom(session);
|
|
ssh_kbdint_free(session->kbdint);
|
|
session->kbdint = NULL;
|
|
|
|
return SSH_PACKET_USED;
|
|
}
|
|
|
|
for (i = 0; i < nprompts; i++) {
|
|
rc = ssh_buffer_unpack(packet, "sb",
|
|
&session->kbdint->prompts[i],
|
|
&session->kbdint->echo[i]);
|
|
if (rc == SSH_ERROR) {
|
|
ssh_set_error(session, SSH_FATAL, "Short INFO_REQUEST packet");
|
|
ssh_kbdint_free(session->kbdint);
|
|
session->kbdint = NULL;
|
|
|
|
return SSH_PACKET_USED;
|
|
}
|
|
}
|
|
session->auth.state=SSH_AUTH_STATE_INFO;
|
|
|
|
return SSH_PACKET_USED;
|
|
}
|
|
|
|
/**
|
|
* @brief Try to authenticate through the "keyboard-interactive" method.
|
|
*
|
|
* @param[in] session The ssh session to use.
|
|
*
|
|
* @param[in] user The username to authenticate. You can specify NULL if
|
|
* ssh_option_set_username() has been used. You cannot try
|
|
* two different logins in a row.
|
|
*
|
|
* @param[in] submethods Undocumented. Set it to NULL.
|
|
*
|
|
* @returns SSH_AUTH_ERROR: A serious error happened\n
|
|
* SSH_AUTH_DENIED: Authentication failed : use another method\n
|
|
* SSH_AUTH_PARTIAL: You've been partially authenticated, you still
|
|
* have to use another method\n
|
|
* SSH_AUTH_SUCCESS: Authentication success\n
|
|
* SSH_AUTH_INFO: The server asked some questions. Use
|
|
* ssh_userauth_kbdint_getnprompts() and such.\n
|
|
* SSH_AUTH_AGAIN: In nonblocking mode, you've got to call this again
|
|
* later.
|
|
*
|
|
* @see ssh_userauth_kbdint_getnprompts()
|
|
* @see ssh_userauth_kbdint_getname()
|
|
* @see ssh_userauth_kbdint_getinstruction()
|
|
* @see ssh_userauth_kbdint_getprompt()
|
|
* @see ssh_userauth_kbdint_setanswer()
|
|
*/
|
|
int ssh_userauth_kbdint(ssh_session session, const char *user,
|
|
const char *submethods)
|
|
{
|
|
int rc = SSH_AUTH_ERROR;
|
|
|
|
if (session == NULL) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
|
|
if ((session->pending_call_state == SSH_PENDING_CALL_NONE && session->kbdint == NULL) ||
|
|
session->pending_call_state == SSH_PENDING_CALL_AUTH_KBDINT_INIT)
|
|
rc = ssh_userauth_kbdint_init(session, user, submethods);
|
|
else if (session->pending_call_state == SSH_PENDING_CALL_AUTH_KBDINT_SEND ||
|
|
session->kbdint != NULL) {
|
|
/*
|
|
* If we are at this point, it is because session->kbdint exists.
|
|
* It means the user has set some information there we need to send
|
|
* the server and then we need to ack the status (new questions or ok
|
|
* pass in).
|
|
* It is possible that session->kbdint is NULL while we're waiting for
|
|
* a reply, hence the test for the pending call.
|
|
*/
|
|
rc = ssh_userauth_kbdint_send(session);
|
|
} else {
|
|
/* We are here because session->kbdint == NULL & state != NONE.
|
|
* This should not happen
|
|
*/
|
|
rc = SSH_AUTH_ERROR;
|
|
ssh_set_error(session, SSH_FATAL, "Invalid state in %s", __func__);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* @brief Get the number of prompts (questions) the server has given.
|
|
*
|
|
* Once you have called ssh_userauth_kbdint() and received SSH_AUTH_INFO return
|
|
* code, this function can be used to retrieve information about the keyboard
|
|
* interactive authentication questions sent by the remote host.
|
|
*
|
|
* @param[in] session The ssh session to use.
|
|
*
|
|
* @returns The number of prompts.
|
|
*/
|
|
int ssh_userauth_kbdint_getnprompts(ssh_session session)
|
|
{
|
|
if (session == NULL) {
|
|
return SSH_ERROR;
|
|
}
|
|
if (session->kbdint == NULL) {
|
|
ssh_set_error_invalid(session);
|
|
return SSH_ERROR;
|
|
}
|
|
return session->kbdint->nprompts;
|
|
}
|
|
|
|
/**
|
|
* @brief Get the "name" of the message block.
|
|
*
|
|
* Once you have called ssh_userauth_kbdint() and received SSH_AUTH_INFO return
|
|
* code, this function can be used to retrieve information about the keyboard
|
|
* interactive authentication questions sent by the remote host.
|
|
*
|
|
* @param[in] session The ssh session to use.
|
|
*
|
|
* @returns The name of the message block. Do not free it.
|
|
*/
|
|
const char *ssh_userauth_kbdint_getname(ssh_session session)
|
|
{
|
|
if (session == NULL) {
|
|
return NULL;
|
|
}
|
|
if (session->kbdint == NULL) {
|
|
ssh_set_error_invalid(session);
|
|
return NULL;
|
|
}
|
|
return session->kbdint->name;
|
|
}
|
|
|
|
/**
|
|
* @brief Get the "instruction" of the message block.
|
|
*
|
|
* Once you have called ssh_userauth_kbdint() and received SSH_AUTH_INFO return
|
|
* code, this function can be used to retrieve information about the keyboard
|
|
* interactive authentication questions sent by the remote host.
|
|
*
|
|
* @param[in] session The ssh session to use.
|
|
*
|
|
* @returns The instruction of the message block.
|
|
*/
|
|
|
|
const char *ssh_userauth_kbdint_getinstruction(ssh_session session)
|
|
{
|
|
if (session == NULL)
|
|
return NULL;
|
|
if (session->kbdint == NULL) {
|
|
ssh_set_error_invalid(session);
|
|
return NULL;
|
|
}
|
|
return session->kbdint->instruction;
|
|
}
|
|
|
|
/**
|
|
* @brief Get a prompt from a message block.
|
|
*
|
|
* Once you have called ssh_userauth_kbdint() and received SSH_AUTH_INFO return
|
|
* code, this function can be used to retrieve information about the keyboard
|
|
* interactive authentication questions sent by the remote host.
|
|
*
|
|
* @param[in] session The ssh session to use.
|
|
*
|
|
* @param[in] i The index number of the i'th prompt.
|
|
*
|
|
* @param[out] echo This is an optional variable. You can obtain a
|
|
* boolean if the user input should be echoed or
|
|
* hidden. For passwords it is usually hidden.
|
|
*
|
|
* @returns A pointer to the prompt. Do not free it.
|
|
*
|
|
* @code
|
|
* const char prompt;
|
|
* char echo;
|
|
*
|
|
* prompt = ssh_userauth_kbdint_getprompt(session, 0, &echo);
|
|
* if (echo) ...
|
|
* @endcode
|
|
*/
|
|
const char *
|
|
ssh_userauth_kbdint_getprompt(ssh_session session, unsigned int i, char *echo)
|
|
{
|
|
if (session == NULL)
|
|
return NULL;
|
|
if (session->kbdint == NULL) {
|
|
ssh_set_error_invalid(session);
|
|
return NULL;
|
|
}
|
|
if (i > session->kbdint->nprompts) {
|
|
ssh_set_error_invalid(session);
|
|
return NULL;
|
|
}
|
|
|
|
if (echo) {
|
|
*echo = (char)session->kbdint->echo[i];
|
|
}
|
|
|
|
return session->kbdint->prompts[i];
|
|
}
|
|
|
|
#ifdef WITH_SERVER
|
|
/**
|
|
* @brief Get the number of answers the client has given.
|
|
*
|
|
* @param[in] session The ssh session to use.
|
|
*
|
|
* @returns The number of answers.
|
|
*/
|
|
int ssh_userauth_kbdint_getnanswers(ssh_session session)
|
|
{
|
|
if (session == NULL || session->kbdint == NULL) {
|
|
return SSH_ERROR;
|
|
}
|
|
return session->kbdint->nanswers;
|
|
}
|
|
|
|
/**
|
|
* @brief Get the answer to a question from a message block.
|
|
*
|
|
* @param[in] session The ssh session to use.
|
|
*
|
|
* @param[in] i index The number of the ith answer.
|
|
*
|
|
* @return The answer string, or NULL if the answer is not
|
|
* available. Do not free the string.
|
|
*/
|
|
const char *ssh_userauth_kbdint_getanswer(ssh_session session, unsigned int i)
|
|
{
|
|
if (session == NULL || session->kbdint == NULL
|
|
|| session->kbdint->answers == NULL) {
|
|
return NULL;
|
|
}
|
|
if (i >= session->kbdint->nanswers) {
|
|
return NULL;
|
|
}
|
|
|
|
return session->kbdint->answers[i];
|
|
}
|
|
#endif
|
|
|
|
/**
|
|
* @brief Set the answer for a question from a message block.
|
|
*
|
|
* If you have called ssh_userauth_kbdint() and got SSH_AUTH_INFO, this
|
|
* function returns the questions from the server.
|
|
*
|
|
* @param[in] session The ssh session to use.
|
|
*
|
|
* @param[in] i index The number of the ith prompt.
|
|
*
|
|
* @param[in] answer The answer to give to the server. The answer MUST be
|
|
* encoded UTF-8. It is up to the server how to interpret
|
|
* the value and validate it. However, if you read the
|
|
* answer in some other encoding, you MUST convert it to
|
|
* UTF-8.
|
|
*
|
|
* @return 0 on success, < 0 on error.
|
|
*/
|
|
int
|
|
ssh_userauth_kbdint_setanswer(ssh_session session, unsigned int i,
|
|
const char *answer)
|
|
{
|
|
if (session == NULL) {
|
|
return -1;
|
|
}
|
|
if (answer == NULL || session->kbdint == NULL ||
|
|
i >= session->kbdint->nprompts) {
|
|
ssh_set_error_invalid(session);
|
|
return -1;
|
|
}
|
|
|
|
if (session->kbdint->answers == NULL) {
|
|
session->kbdint->answers = calloc(session->kbdint->nprompts, sizeof(char *));
|
|
if (session->kbdint->answers == NULL) {
|
|
ssh_set_error_oom(session);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (session->kbdint->answers[i]) {
|
|
explicit_bzero(session->kbdint->answers[i],
|
|
strlen(session->kbdint->answers[i]));
|
|
SAFE_FREE(session->kbdint->answers[i]);
|
|
}
|
|
|
|
session->kbdint->answers[i] = strdup(answer);
|
|
if (session->kbdint->answers[i] == NULL) {
|
|
ssh_set_error_oom(session);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Try to authenticate through the "gssapi-with-mic" method.
|
|
*
|
|
* @param[in] session The ssh session to use.
|
|
*
|
|
* @returns SSH_AUTH_ERROR: A serious error happened\n
|
|
* SSH_AUTH_DENIED: Authentication failed : use another method\n
|
|
* SSH_AUTH_PARTIAL: You've been partially authenticated, you still
|
|
* have to use another method\n
|
|
* SSH_AUTH_SUCCESS: Authentication success\n
|
|
* SSH_AUTH_AGAIN: In nonblocking mode, you've got to call this again
|
|
* later.
|
|
*/
|
|
int ssh_userauth_gssapi(ssh_session session)
|
|
{
|
|
int rc = SSH_AUTH_DENIED;
|
|
#ifdef WITH_GSSAPI
|
|
switch(session->pending_call_state) {
|
|
case SSH_PENDING_CALL_NONE:
|
|
break;
|
|
case SSH_PENDING_CALL_AUTH_GSSAPI_MIC:
|
|
goto pending;
|
|
default:
|
|
ssh_set_error(session,
|
|
SSH_FATAL,
|
|
"Wrong state (%d) during pending SSH call",
|
|
session->pending_call_state);
|
|
return SSH_ERROR;
|
|
}
|
|
|
|
rc = ssh_userauth_request_service(session);
|
|
if (rc == SSH_AGAIN) {
|
|
return SSH_AUTH_AGAIN;
|
|
} else if (rc == SSH_ERROR) {
|
|
return SSH_AUTH_ERROR;
|
|
}
|
|
SSH_LOG(SSH_LOG_DEBUG, "Authenticating with gssapi-with-mic");
|
|
|
|
session->auth.current_method = SSH_AUTH_METHOD_GSSAPI_MIC;
|
|
session->auth.state = SSH_AUTH_STATE_NONE;
|
|
session->pending_call_state = SSH_PENDING_CALL_AUTH_GSSAPI_MIC;
|
|
rc = ssh_gssapi_auth_mic(session);
|
|
|
|
if (rc == SSH_AUTH_ERROR || rc == SSH_AUTH_DENIED) {
|
|
session->auth.state = SSH_AUTH_STATE_NONE;
|
|
session->pending_call_state = SSH_PENDING_CALL_NONE;
|
|
return rc;
|
|
}
|
|
|
|
pending:
|
|
rc = ssh_userauth_get_response(session);
|
|
if (rc != SSH_AUTH_AGAIN) {
|
|
session->pending_call_state = SSH_PENDING_CALL_NONE;
|
|
}
|
|
#else
|
|
(void) session; /* unused */
|
|
#endif
|
|
return rc;
|
|
}
|
|
|
|
/** @} */
|