Files
libssh/src/kex-gss.c
Jakub Jelen 59ed66b684 New ssh_get_local_hostname()
Signed-off-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
2026-02-05 15:43:11 +01:00

672 lines
21 KiB
C

/*
* kex-gss.c - GSSAPI key exchange
*
* This file is part of the SSH Library
*
* Copyright (c) 2024 by Gauravsingh Sisodia <xaerru@gmail.com>
*
* 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 "libssh/gssapi.h"
#include <errno.h>
#include <gssapi/gssapi.h>
#include <stdio.h>
#include "libssh/buffer.h"
#include "libssh/crypto.h"
#include "libssh/kex-gss.h"
#include "libssh/bignum.h"
#include "libssh/curve25519.h"
#include "libssh/ecdh.h"
#include "libssh/dh.h"
#include "libssh/priv.h"
#include "libssh/session.h"
#include "libssh/ssh2.h"
static SSH_PACKET_CALLBACK(ssh_packet_client_gss_kex_reply);
static ssh_packet_callback gss_kex_client_callbacks[] = {
ssh_packet_client_gss_kex_reply,
};
static struct ssh_packet_callbacks_struct ssh_gss_kex_client_callbacks = {
.start = SSH2_MSG_KEXGSS_COMPLETE,
.n_callbacks = 1,
.callbacks = gss_kex_client_callbacks,
.user = NULL,
};
static SSH_PACKET_CALLBACK(ssh_packet_client_gss_kex_hostkey);
static ssh_packet_callback gss_kex_client_callback_hostkey[] = {
ssh_packet_client_gss_kex_hostkey,
};
static struct ssh_packet_callbacks_struct ssh_gss_kex_client_callback_hostkey = {
.start = SSH2_MSG_KEXGSS_HOSTKEY,
.n_callbacks = 1,
.callbacks = gss_kex_client_callback_hostkey,
.user = NULL,
};
static ssh_string dh_init(ssh_session session)
{
int rc, keypair;
#if !defined(HAVE_LIBCRYPTO) || OPENSSL_VERSION_NUMBER < 0x30000000L
const_bignum const_pubkey;
#endif
bignum pubkey = NULL;
ssh_string pubkey_string = NULL;
struct ssh_crypto_struct *crypto = session->next_crypto;
if (session->server) {
keypair = DH_SERVER_KEYPAIR;
} else {
keypair = DH_CLIENT_KEYPAIR;
}
rc = ssh_dh_init_common(crypto);
if (rc != SSH_OK) {
goto end;
}
rc = ssh_dh_keypair_gen_keys(crypto->dh_ctx, keypair);
if (rc != SSH_OK) {
goto end;
}
#if !defined(HAVE_LIBCRYPTO) || OPENSSL_VERSION_NUMBER < 0x30000000L
rc = ssh_dh_keypair_get_keys(crypto->dh_ctx, keypair, NULL, &const_pubkey);
bignum_dup(const_pubkey, &pubkey);
#else
rc = ssh_dh_keypair_get_keys(crypto->dh_ctx, keypair, NULL, &pubkey);
#endif
if (rc != SSH_OK) {
goto end;
}
pubkey_string = ssh_make_bignum_string(pubkey);
end:
bignum_safe_free(pubkey);
return pubkey_string;
}
static int dh_import_peer_key(ssh_session session, ssh_string peer_key)
{
int rc, keypair;
bignum peer_key_bn;
struct ssh_crypto_struct *crypto = session->next_crypto;
if (session->server) {
keypair = DH_CLIENT_KEYPAIR;
} else {
keypair = DH_SERVER_KEYPAIR;
}
peer_key_bn = ssh_make_string_bn(peer_key);
rc = ssh_dh_keypair_set_keys(crypto->dh_ctx, keypair, NULL, peer_key_bn);
if (rc != SSH_OK) {
bignum_safe_free(peer_key_bn);
}
return rc;
}
/** @internal
* @brief Starts gssapi key exchange
*/
int ssh_client_gss_kex_init(ssh_session session)
{
struct ssh_crypto_struct *crypto = session->next_crypto;
int rc, ret = SSH_ERROR;
/* oid selected for authentication */
gss_OID_set selected = GSS_C_NO_OID_SET;
OM_uint32 maj_stat, min_stat;
const char *gss_host = session->opts.host;
gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER;
gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER;
OM_uint32 oflags;
ssh_string pubkey = NULL;
switch (crypto->kex_type) {
case SSH_GSS_KEX_DH_GROUP14_SHA256:
case SSH_GSS_KEX_DH_GROUP16_SHA512:
pubkey = dh_init(session);
if (pubkey == NULL) {
ssh_set_error(session, SSH_FATAL, "Failed to generate DH keypair");
goto out;
}
break;
case SSH_GSS_KEX_ECDH_NISTP256_SHA256:
rc = ssh_ecdh_init(session);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to generate ECDH keypair");
goto out;
}
pubkey = ssh_string_copy(crypto->ecdh_client_pubkey);
break;
case SSH_GSS_KEX_CURVE25519_SHA256:
rc = ssh_curve25519_init(session);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to generate Curve25519 keypair");
goto out;
}
pubkey = ssh_string_new(CURVE25519_PUBKEY_SIZE);
if (pubkey == NULL) {
ssh_set_error_oom(session);
goto out;
}
rc = ssh_string_fill(pubkey,
crypto->curve25519_client_pubkey,
CURVE25519_PUBKEY_SIZE);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to copy Curve25519 pubkey");
goto out;
}
break;
default:
ssh_set_error(session, SSH_FATAL, "Unsupported GSSAPI KEX method");
goto out;
}
rc = ssh_gssapi_init(session);
if (rc != SSH_OK) {
goto out;
}
if (session->opts.gss_server_identity != NULL) {
gss_host = session->opts.gss_server_identity;
}
rc = ssh_gssapi_import_name(session->gssapi, gss_host);
if (rc != SSH_OK) {
goto out;
}
rc = ssh_gssapi_client_identity(session, &selected);
if (rc != SSH_OK) {
goto out;
}
session->gssapi->client.flags = GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG;
maj_stat = ssh_gssapi_init_ctx(session->gssapi,
&input_token,
&output_token,
&oflags);
gss_release_oid_set(&min_stat, &selected);
if (GSS_ERROR(maj_stat)) {
ssh_gssapi_log_error(SSH_LOG_WARN,
"Initializing gssapi context",
maj_stat,
min_stat);
goto out;
}
if (!(oflags & GSS_C_INTEG_FLAG) || !(oflags & GSS_C_MUTUAL_FLAG)) {
SSH_LOG(SSH_LOG_WARN,
"GSSAPI(init) integrity and mutual flags were not set");
goto out;
}
rc = ssh_buffer_pack(session->out_buffer,
"bdPS",
SSH2_MSG_KEXGSS_INIT,
output_token.length,
(size_t)output_token.length,
output_token.value,
pubkey);
if (rc != SSH_OK) {
goto out;
}
/* register the packet callbacks */
ssh_packet_set_callbacks(session, &ssh_gss_kex_client_callbacks);
ssh_packet_set_callbacks(session, &ssh_gss_kex_client_callback_hostkey);
session->dh_handshake_state = DH_STATE_INIT_SENT;
rc = ssh_packet_send(session);
if (rc != SSH_OK) {
goto out;
}
ret = SSH_OK;
out:
gss_release_buffer(&min_stat, &output_token);
ssh_string_free(pubkey);
return ret;
}
void ssh_client_gss_kex_remove_callbacks(ssh_session session)
{
ssh_packet_remove_callbacks(session, &ssh_gss_kex_client_callbacks);
}
void ssh_client_gss_kex_remove_callback_hostkey(ssh_session session)
{
ssh_packet_remove_callbacks(session, &ssh_gss_kex_client_callback_hostkey);
}
SSH_PACKET_CALLBACK(ssh_packet_client_gss_kex_reply)
{
struct ssh_crypto_struct *crypto = session->next_crypto;
ssh_string mic = NULL, otoken = NULL, server_pubkey = NULL;
uint8_t b;
int rc;
gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER;
gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER;
OM_uint32 oflags;
OM_uint32 maj_stat;
(void)type;
(void)user;
ssh_client_gss_kex_remove_callbacks(session);
rc = ssh_buffer_unpack(packet, "SSbS", &server_pubkey, &mic, &b, &otoken);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "No public key in server reply");
goto error;
}
SSH_STRING_FREE(session->gssapi_key_exchange_mic);
session->gssapi_key_exchange_mic = mic;
input_token.length = ssh_string_len(otoken);
input_token.value = ssh_string_data(otoken);
maj_stat = ssh_gssapi_init_ctx(session->gssapi,
&input_token,
&output_token,
&oflags);
if (maj_stat != GSS_S_COMPLETE) {
goto error;
}
SSH_STRING_FREE(otoken);
switch (crypto->kex_type) {
case SSH_GSS_KEX_DH_GROUP14_SHA256:
case SSH_GSS_KEX_DH_GROUP16_SHA512:
rc = dh_import_peer_key(session, server_pubkey);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Could not import server pubkey");
goto error;
}
rc = ssh_dh_compute_shared_secret(crypto->dh_ctx,
DH_CLIENT_KEYPAIR,
DH_SERVER_KEYPAIR,
&crypto->shared_secret);
ssh_dh_debug_crypto(crypto);
break;
case SSH_GSS_KEX_ECDH_NISTP256_SHA256:
crypto->ecdh_server_pubkey = ssh_string_copy(server_pubkey);
rc = ecdh_build_k(session);
break;
case SSH_GSS_KEX_CURVE25519_SHA256:
memcpy(crypto->curve25519_server_pubkey,
ssh_string_data(server_pubkey),
CURVE25519_PUBKEY_SIZE);
rc = ssh_curve25519_build_k(session);
break;
default:
ssh_set_error(session, SSH_FATAL, "Unsupported GSSAPI KEX method");
goto error;
}
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Could not derive shared secret");
goto error;
}
/* Send the MSG_NEWKEYS */
rc = ssh_packet_send_newkeys(session);
if (rc == SSH_ERROR) {
goto error;
}
ssh_string_free(server_pubkey);
session->dh_handshake_state = DH_STATE_NEWKEYS_SENT;
return SSH_PACKET_USED;
error:
ssh_string_free(server_pubkey);
session->session_state = SSH_SESSION_STATE_ERROR;
return SSH_PACKET_USED;
}
SSH_PACKET_CALLBACK(ssh_packet_client_gss_kex_hostkey)
{
ssh_string pubkey_blob = NULL;
int rc;
(void)type;
(void)user;
ssh_client_gss_kex_remove_callback_hostkey(session);
rc = ssh_buffer_unpack(packet, "S", &pubkey_blob);
if (rc == SSH_ERROR) {
ssh_set_error(session,
SSH_FATAL,
"Invalid SSH2_MSG_KEXGSS_HOSTKEY packet");
goto error;
}
rc = ssh_dh_import_next_pubkey_blob(session, pubkey_blob);
SSH_STRING_FREE(pubkey_blob);
if (rc != 0) {
goto error;
}
return SSH_PACKET_USED;
error:
session->session_state = SSH_SESSION_STATE_ERROR;
return SSH_PACKET_USED;
}
#ifdef WITH_SERVER
static SSH_PACKET_CALLBACK(ssh_packet_server_gss_kex_init);
static ssh_packet_callback gss_kex_server_callbacks[] = {
ssh_packet_server_gss_kex_init,
};
static struct ssh_packet_callbacks_struct ssh_gss_kex_server_callbacks = {
.start = SSH2_MSG_KEXGSS_INIT,
.n_callbacks = 1,
.callbacks = gss_kex_server_callbacks,
.user = NULL,
};
/** @internal
* @brief sets up the gssapi kex callbacks
*/
void ssh_server_gss_kex_init(ssh_session session)
{
/* register the packet callbacks */
ssh_packet_set_callbacks(session, &ssh_gss_kex_server_callbacks);
}
/** @internal
* @brief processes a SSH_MSG_KEXGSS_INIT and sends
* the appropriate SSH_MSG_KEXGSS_COMPLETE
*/
int ssh_server_gss_kex_process_init(ssh_session session, ssh_buffer packet)
{
struct ssh_crypto_struct *crypto = session->next_crypto;
ssh_key privkey = NULL;
enum ssh_digest_e digest = SSH_DIGEST_AUTO;
ssh_string client_pubkey = NULL;
ssh_string server_pubkey = NULL;
int rc;
gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER;
gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER;
ssh_string otoken = NULL;
ssh_string server_pubkey_blob = NULL;
OM_uint32 maj_stat, min_stat;
gss_name_t client_name = GSS_C_NO_NAME;
OM_uint32 ret_flags = 0;
gss_buffer_desc mic = GSS_C_EMPTY_BUFFER, msg = GSS_C_EMPTY_BUFFER;
char *hostname = NULL;
char err_msg[SSH_ERRNO_MSG_MAX] = {0};
rc = ssh_buffer_unpack(packet, "S", &otoken);
if (rc == SSH_ERROR) {
ssh_set_error(session, SSH_FATAL, "No token in client request");
goto error;
}
input_token.length = ssh_string_len(otoken);
input_token.value = ssh_string_data(otoken);
rc = ssh_buffer_unpack(packet, "S", &client_pubkey);
if (rc == SSH_ERROR) {
ssh_set_error(session, SSH_FATAL, "No public key in client request");
goto error;
}
switch (crypto->kex_type) {
case SSH_GSS_KEX_DH_GROUP14_SHA256:
case SSH_GSS_KEX_DH_GROUP16_SHA512:
server_pubkey = dh_init(session);
if (server_pubkey == NULL) {
ssh_set_error(session, SSH_FATAL, "Could not generate a DH keypair");
goto error;
}
rc = dh_import_peer_key(session, client_pubkey);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Could not import client pubkey");
goto error;
}
rc = ssh_dh_compute_shared_secret(crypto->dh_ctx,
DH_SERVER_KEYPAIR,
DH_CLIENT_KEYPAIR,
&crypto->shared_secret);
ssh_dh_debug_crypto(crypto);
break;
case SSH_GSS_KEX_ECDH_NISTP256_SHA256:
rc = ssh_ecdh_init(session);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Could not generate an ECDH keypair");
goto error;
}
crypto->ecdh_client_pubkey = ssh_string_copy(client_pubkey);
server_pubkey = ssh_string_copy(crypto->ecdh_server_pubkey);
rc = ecdh_build_k(session);
break;
case SSH_GSS_KEX_CURVE25519_SHA256:
rc = ssh_curve25519_init(session);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Could not generate a Curve25519 keypair");
goto error;
}
server_pubkey = ssh_string_new(CURVE25519_PUBKEY_SIZE);
if (server_pubkey == NULL) {
ssh_set_error_oom(session);
goto error;
}
rc = ssh_string_fill(server_pubkey,
crypto->curve25519_server_pubkey,
CURVE25519_PUBKEY_SIZE);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Failed to copy Curve25519 pubkey");
goto error;
}
memcpy(crypto->curve25519_client_pubkey,
ssh_string_data(client_pubkey),
CURVE25519_PUBKEY_SIZE);
rc = ssh_curve25519_build_k(session);
break;
default:
ssh_set_error(session, SSH_FATAL, "Unsupported GSSAPI KEX method");
goto error;
}
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Could not derive shared secret");
goto error;
}
/* Also imports next_crypto->server_pubkey
* Can give error when using null hostkey */
ssh_get_key_params(session, &privkey, &digest);
rc = ssh_make_sessionid(session);
if (rc != SSH_OK) {
ssh_set_error(session, SSH_FATAL, "Could not create a session id");
goto error;
}
if (strcmp(crypto->kex_methods[SSH_HOSTKEYS], "null") != 0) {
rc =
ssh_dh_get_next_server_publickey_blob(session, &server_pubkey_blob);
if (rc != SSH_OK) {
goto error;
}
rc = ssh_buffer_pack(session->out_buffer,
"bS",
SSH2_MSG_KEXGSS_HOSTKEY,
server_pubkey_blob);
if (rc != SSH_OK) {
ssh_set_error_oom(session);
ssh_buffer_reinit(session->out_buffer);
goto error;
}
rc = ssh_packet_send(session);
if (rc == SSH_ERROR) {
goto error;
}
SSH_LOG(SSH_LOG_DEBUG, "Sent SSH2_MSG_KEXGSS_HOSTKEY");
SSH_STRING_FREE(server_pubkey_blob);
}
rc = ssh_gssapi_init(session);
if (rc == SSH_ERROR) {
goto error;
}
hostname = ssh_get_local_hostname();
if (hostname == NULL) {
SSH_LOG(SSH_LOG_TRACE,
"Error getting hostname: %s",
ssh_strerror(errno, err_msg, SSH_ERRNO_MSG_MAX));
goto error;
}
rc = ssh_gssapi_import_name(session->gssapi, hostname);
SAFE_FREE(hostname);
if (rc != SSH_OK) {
goto error;
}
maj_stat = gss_acquire_cred(&min_stat,
session->gssapi->client.server_name,
0,
GSS_C_NO_OID_SET,
GSS_C_ACCEPT,
&session->gssapi->server_creds,
NULL,
NULL);
if (maj_stat != GSS_S_COMPLETE) {
ssh_gssapi_log_error(SSH_LOG_TRACE,
"acquiring credentials",
maj_stat,
min_stat);
goto error;
}
maj_stat = gss_accept_sec_context(&min_stat,
&session->gssapi->ctx,
session->gssapi->server_creds,
&input_token,
GSS_C_NO_CHANNEL_BINDINGS,
&client_name,
NULL /*mech_oid*/,
&output_token,
&ret_flags,
NULL /*time*/,
&session->gssapi->client_creds);
if (GSS_ERROR(maj_stat)) {
ssh_gssapi_log_error(SSH_LOG_DEBUG,
"accepting token failed",
maj_stat,
min_stat);
goto error;
}
SSH_STRING_FREE(otoken);
gss_release_name(&min_stat, &client_name);
if (!(ret_flags & GSS_C_INTEG_FLAG) || !(ret_flags & GSS_C_MUTUAL_FLAG)) {
SSH_LOG(SSH_LOG_WARN,
"GSSAPI(accept) integrity and mutual flags were not set");
goto error;
}
SSH_LOG(SSH_LOG_DEBUG, "token accepted");
msg.length = session->next_crypto->digest_len;
msg.value = session->next_crypto->secret_hash;
maj_stat = gss_get_mic(&min_stat,
session->gssapi->ctx,
GSS_C_QOP_DEFAULT,
&msg,
&mic);
if (GSS_ERROR(maj_stat)) {
ssh_gssapi_log_error(SSH_LOG_DEBUG,
"creating mic failed",
maj_stat,
min_stat);
goto error;
}
rc = ssh_buffer_pack(session->out_buffer,
"bSdPbdP",
SSH2_MSG_KEXGSS_COMPLETE,
server_pubkey,
mic.length,
(size_t)mic.length,
mic.value,
1,
output_token.length,
(size_t)output_token.length,
output_token.value);
if (rc != SSH_OK) {
ssh_set_error_oom(session);
ssh_buffer_reinit(session->out_buffer);
goto error;
}
gss_release_buffer(&min_stat, &output_token);
gss_release_buffer(&min_stat, &mic);
rc = ssh_packet_send(session);
if (rc == SSH_ERROR) {
goto error;
}
SSH_LOG(SSH_LOG_DEBUG, "Sent SSH2_MSG_KEXGSS_COMPLETE");
session->dh_handshake_state = DH_STATE_NEWKEYS_SENT;
/* Send the MSG_NEWKEYS */
rc = ssh_packet_send_newkeys(session);
if (rc == SSH_ERROR) {
goto error;
}
ssh_string_free(server_pubkey);
ssh_string_free(client_pubkey);
return SSH_OK;
error:
SSH_STRING_FREE(server_pubkey_blob);
ssh_string_free(server_pubkey);
ssh_string_free(client_pubkey);
session->session_state = SSH_SESSION_STATE_ERROR;
return SSH_ERROR;
}
/** @internal
* @brief parse an incoming SSH_MSG_KEXGSS_INIT packet and complete
* Diffie-Hellman key exchange
**/
static SSH_PACKET_CALLBACK(ssh_packet_server_gss_kex_init)
{
(void)type;
(void)user;
SSH_LOG(SSH_LOG_DEBUG, "Received SSH_MSG_KEXGSS_INIT");
ssh_packet_remove_callbacks(session, &ssh_gss_kex_server_callbacks);
ssh_server_gss_kex_process_init(session, packet);
return SSH_PACKET_USED;
}
#endif /* WITH_SERVER */