Compare commits

...

31 Commits

Author SHA1 Message Date
Arthur Chan
4dfcdd96b8 OSS-Fuzz: Add fuzzer for scp functions
Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-13 20:48:53 +01:00
Emmanuel Ugwu
9d36b9dd81 docs: add doxygen documentation and fix inconsistencies
- src/misc.c: added doxygen docs for ssh_get_local_username()
- src/auth.c: added doxygen docs for ssh_kbdint_new(), ssh_kbdint_free(), ssh_kbdint_clean()
- src/bind_config.c: fix @params -> @param, @returns -> @return
- src/bind.c, src/socket.c, src/threads.c: fix @returns -> @return
- include/libssh/callbacks.h: fix @returns -> @return

Signed-off-by: Emmanuel Ugwu <emmanuelugwu121@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-13 20:48:07 +01:00
Rui Li
afa21334b4 tests: Add tests for originalhost/host separation and Match support
Signed-off-by: Rui Li <ruili3422@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-13 20:46:35 +01:00
Rui Li
a2ebc7ea9b Implement originalhost/host separation and Match support
Signed-off-by: Rui Li <ruili3422@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-13 20:46:35 +01:00
Rui Li
1ab8a35c5d Add strict validation mode to ssh_config_parse_uri in config_parser
Signed-off-by: Rui Li <ruili3422@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-13 20:46:35 +01:00
Madhav Vasisth
8782fcec18 agent: Add support for SSH2_AGENTC_REMOVE_IDENTITY
Implement support for the SSH2_AGENTC_REMOVE_IDENTITY
agent protocol message.

The implementation mirrors ssh_agent_sign_data()
and reuses agent_talk(). A single cleanup path is
used to ensure proper resource handling.

Signed-off-by: Madhav Vasisth <mv2363@srmist.edu.in>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-09 18:27:10 +01:00
Pavol Žáčik
8d563f90f3 Add more krb5-related Valgrind suppressions
All newly reported leaks are categorized as
reachable and they mostly relate to global
variables in krb5 which are free'd before
each re-initialization.

Fixes #352.

Signed-off-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-09 18:20:06 +01:00
Jakub Jelen
6a5e298cec Log more useful information to be able to troubleshoot sftp server
Signed-off-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
2026-03-06 15:02:37 +01:00
Jan Pazdziora
163e1b059b Expansion of %s expansion is no longer happening.
The SSH_OPTIONS_SSH_DIR/session->opts.sshdir value
is passed through ssh_path_expand_tilde which does not expand %s.

Amending f643c34ee8.

Signed-off-by: Jan Pazdziora <jan.pazdziora@code.adelton.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-06 15:02:37 +01:00
Michael Hansen
e16018491e Add casts to a couple more pack size constants in hybrid_mlkem.c
Signed-off-by: Michael Hansen <zrax0111@gmail.com>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-06 15:02:37 +01:00
Michael Hansen
c26e9298e3 Fix parameter size mismatch in ssh_buffer_pack for hybrid_mlkem.c
Signed-off-by: Michael Hansen <zrax0111@gmail.com>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-06 15:02:37 +01:00
Shiva Kiran Koninty
3c0567cb67 docs: Fix struct field comment positioning for Doxygen
Doxygen interprets comments placed beside struct fields to belong
to the next field instead of the current field.

This could be fixed by moving the comments atop the fields,
or by using the `/**< COMMENT */` format.

Stay consistent with the comment format used for other structs
and move the comments atop the fields.

Signed-off-by: Shiva Kiran Koninty <shiva_kr@riseup.net>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
2026-03-06 15:02:37 +01:00
Shiva Kiran Koninty
00d1903bf6 doc: Document sftp_attributes_struct
Fixes #333

Signed-off-by: Shiva Kiran Koninty <shiva_kr@riseup.net>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-06 15:02:37 +01:00
Himaneesh Mishra
bc2a483aa1 headers: add missing stdint/stddef includes
Signed-off-by: Himaneesh Mishra <himaneeshmishra@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-06 15:02:37 +01:00
Shiva Kiran Koninty
5ad8dda6f6 buffer: Remove support for format specifier 'F' in ssh_buffer_pack()
Eliminate dead code.

Signed-off-by: Shiva Kiran Koninty <shiva_kr@riseup.net>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-06 15:02:37 +01:00
Shiva Kiran Koninty
d680b8ea8a sntrup: Remove needless conversion of shared secret to bignum
The derived shared secret in SNTRUP761 is converted into a bignum,
only to be converted back to binary during use in kex.c.
Instead use field 'hybrid_shared_secret' in ssh_crypto_struct
to store it, just like the Hybrid MLKEM implementation.

Fixes #338

Signed-off-by: Shiva Kiran Koninty <shiva_kr@riseup.net>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-06 15:02:37 +01:00
Francesco Rollo
90b07e2c18 refactor(server): Warn about config override behavior in bind APIs
- Add a warning to ssh_bind_listen() clarifying that it implicitly
calls ssh_bind_options_parse_config(), which may override options
previously set via ssh_bind_options_set().

- Add a warning to ssh_bind_options_set() and ssh_bind_config_parse_string()
explaining that options may be overridden if configuration files are parsed
afterwards, either implicitly via ssh_bind_listen() or by an explicit call to
ssh_bind_options_parse_config().

Signed-off-by: Francesco <eferollo@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:36:59 +01:00
Francesco Rollo
edbd929fa2 feat(server): Add support for -o option argument in server example
Allow passing server configuration options via the -o flag and expose
ssh_bind_config_parse_string() as a public API.

Signed-off-by: Francesco <eferollo@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:36:58 +01:00
Himaneesh Mishra
38932b74c0 docs: reduce Doxygen warnings in libsshpp.hpp
Signed-off-by: Himaneesh Mishra <himaneeshmishra@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:35:41 +01:00
Mingyuan Li
60d6179eaa tests: Add opendir handle exhaustion test for sftpserver
Add torture_server_sftp_opendir_handles_exhaustion test that
exercises the error path in process_opendir() when all SFTP
handles are occupied. This covers the memory leak fix for
h->name that was missing in the sftp_handle_alloc() failure path.

The test exhausts all 256 handle slots with sftp_open(), then
verifies that sftp_opendir() fails gracefully without crashing
or leaking memory.

Signed-off-by: Mingyuan Li <2560359315@qq.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:33:36 +01:00
Mingyuan Li
0d9b2c68cc sftpserver: Fix memory leak of h->name in process_opendir error path
When sftp_handle_alloc() fails in process_opendir(), the error path
frees the handle struct h but does not free h->name which was
allocated by strdup(). This causes a memory leak every time the
server runs out of available SFTP handles while processing an
opendir request.

Also add a missing NULL check for the strdup() call itself to
handle out-of-memory conditions gracefully.

This is the same class of bug that was fixed in process_open() by
commit db7f101d (CVE-2025-5449), but was missed in process_opendir().

Signed-off-by: Mingyuan Li <2560359315@qq.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:33:35 +01:00
Madhav Vasisth
adc2462329 docs: clarify ssh-agent API usage and lifecycle
Clarify the session-coupled nature of the ssh-agent interface,
document lifecycle and ownership expectations of agent-related
objects, and describe common error cases and limitations.

No functional changes.

Signed-off-by: Madhav Vasisth <mv2363@srmist.edu.in>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:32:39 +01:00
Pavol Žáčik
0bff33c790 gss-kex: Fix memory leaks in ssh_gssapi_check_client_config
Upon unsuccessful alloc of the gssapi context, the function
would return early without freeing the supported OID set.

With opts->gss_client_identity enabled, the function would
not free the client_id allocated by gss_import_name.

Signed-off-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:30:53 +01:00
Pavol Žáčik
47e9b5536a gss-kex: Release output_token and mic on error paths
Signed-off-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:30:53 +01:00
Pavol Žáčik
2f1f474e27 gssapi: Free both_supported on a new error path
Signed-off-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:30:52 +01:00
Bulitha Kawushika De Zoysa
18d7a3967c tests: Add interoperability tests against TinySSH
This adds a new test suite 'torture_tinyssh' that verifies interoperability with the TinySSH server using various key exchange methods.

Fixes #271

Signed-off-by: Bulitha Kawushika De Zoysa <bulithakaushika99@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-20 10:56:46 +01:00
Antoni Bertolin Monferrer
d45ce10c83 channels: Fix OOM error check after strdup
The fix allows the code to properly check if the strdup failed to allocate a
char buffer for the exit signal.

Signed-off-by: Antoni Bertolin Monferrer <antoni.monferrer@canonical.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-20 10:55:17 +01:00
Pavol Žáčik
a7fd80795e Update recently added logging to be less verbose
In 20d9642c and parent commits, log levels were
recategorized to be less verbose when using the
level INFO and lower. These levels should not
print any information redundant to the end user.

This commit fixes recently added uses of logging
that are not consistent with the abovementioned
categorization, in particular:

- logs in ssh_strict_fopen should not have
  the RARE/WARNING level since failing to open
  a file may not be an issue at all (e.g., when
  trying to open the knownhosts file).

- logging the username used in authentication
  or proxyjump-related information should be done
  at the DEBUG level, otherwise it could pollute
  the output of, e.g., curl.

Signed-off-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-20 10:54:27 +01:00
Jakub Jelen
f8cba20859 Add back Security section to 0.12.0 changelog
Signed-off-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
2026-02-12 14:54:06 +01:00
Arthur Chan
f13a8d7ced OSS-Fuzz Add fuzzer and corpora for sftp attr parsing
Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-10 15:34:39 +01:00
Shreyas Mahajan
c0963b3417 SSH2 NONE authentication
Signed-off-by: Shreyas Mahajan <shreyasmahajan05@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-10 15:16:58 +01:00
84 changed files with 2691 additions and 290 deletions

View File

@@ -2,6 +2,17 @@ CHANGELOG
=========
version 0.12.0 (released 2026-02-10)
* Security:
* CVE-2025-14821: libssh loads configuration files from the C:\etc directory
on Windows
* CVE-2026-0964: SCP Protocol Path Traversal in ssh_scp_pull_request()
* CVE-2026-0965: Possible Denial of Service when parsing unexpected
configuration files
* CVE-2026-0966: Buffer underflow in ssh_get_hexa() on invalid input
* CVE-2026-0967: Specially crafted patterns could cause DoS
* CVE-2026-0968: OOB Read in sftp_parse_longname()
* libssh-2026-sftp-extensions: Read buffer overrun when handling SFTP
extensions
* Deprecations and removals:
* Bumped minimal RSA key size to 1024 bits
* New functionality:

View File

@@ -108,7 +108,6 @@ if (DOXYGEN_FOUND)
packet_struct,
pem_get_password_struct,
ssh_tokens_st,
sftp_attributes_struct,
sftp_client_message_struct,
sftp_dir_struct,
sftp_ext_struct,

View File

@@ -337,6 +337,7 @@ static void batch_shell(ssh_session session)
static int client(ssh_session session)
{
int auth = 0;
int authenticated = 0;
char *banner = NULL;
int state;
@@ -369,16 +370,28 @@ static int client(ssh_session session)
return -1;
}
ssh_userauth_none(session, NULL);
banner = ssh_get_issue_banner(session);
if (banner) {
printf("%s\n", banner);
free(banner);
}
auth = authenticate_console(session);
if (auth != SSH_AUTH_SUCCESS) {
auth = ssh_userauth_none(session, NULL);
if (auth == SSH_AUTH_SUCCESS) {
authenticated = 1;
} else if (auth == SSH_AUTH_ERROR) {
fprintf(stderr,
"Authentication error during none auth: %s\n",
ssh_get_error(session));
return -1;
}
if (!authenticated) {
auth = authenticate_console(session);
if (auth != SSH_AUTH_SUCCESS) {
return -1;
}
}
if (cmds[0] == NULL) {
shell(session);
} else {

View File

@@ -107,6 +107,14 @@ static struct argp_option options[] = {
.doc = "Set the authorized keys file.",
.group = 0
},
{
.name = "option",
.key = 'o',
.arg = "OPTION",
.flags = 0,
.doc = "Set server configuration option [-o OptionName=Value]",
.group = 0
},
{
.name = "user",
.key = 'u',
@@ -158,6 +166,9 @@ parse_opt(int key, char *arg, struct argp_state *state)
case 'a':
strncpy(authorizedkeys, arg, DEF_STR_SIZE - 1);
break;
case 'o':
ssh_bind_config_parse_string(sshbind, arg);
break;
case 'u':
strncpy(username, arg, sizeof(username) - 1);
break;
@@ -194,7 +205,7 @@ parse_opt(int argc, char **argv, ssh_bind sshbind)
{
int key;
while((key = getopt(argc, argv, "a:e:k:p:P:r:u:v")) != -1) {
while((key = getopt(argc, argv, "a:e:k:o:p:P:r:u:v")) != -1) {
if (key == 'p') {
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDPORT_STR, optarg);
} else if (key == 'k') {
@@ -205,6 +216,8 @@ parse_opt(int argc, char **argv, ssh_bind sshbind)
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HOSTKEY, optarg);
} else if (key == 'a') {
strncpy(authorizedkeys, optarg, DEF_STR_SIZE-1);
} else if (key == 'o') {
ssh_bind_config_parse_string(sshbind, optarg);
} else if (key == 'u') {
strncpy(username, optarg, sizeof(username) - 1);
} else if (key == 'P') {
@@ -222,6 +235,7 @@ parse_opt(int argc, char **argv, ssh_bind sshbind)
"libssh %s -- a Secure Shell protocol implementation\n"
"\n"
" -a, --authorizedkeys=FILE Set the authorized keys file.\n"
" -o, --option=OPTION Set server configuration option (e.g., -o OptionName=Value).\n"
" -e, --ecdsakey=FILE Set the ecdsa key (deprecated alias for 'k').\n"
" -k, --hostkey=FILE Set a host key. Can be used multiple times.\n"
" Implies no default keys.\n"

View File

@@ -74,6 +74,13 @@
extern "C" {
#endif
/**
* @brief SSH agent connection context.
*
* This structure represents a connection to an SSH authentication agent.
* It is used by libssh to communicate with the agent for operations such
* as listing available identities and signing data during authentication.
*/
struct ssh_agent_struct {
struct ssh_socket_struct *sock;
ssh_buffer ident;
@@ -82,13 +89,24 @@ struct ssh_agent_struct {
};
/* agent.c */
/**
* @brief Create a new ssh agent structure.
*
* @return An allocated ssh agent structure or NULL on error.
* Creates and initializes an SSH agent context bound to the given
* SSH session. The agent connection relies on the session configuration
* (e.g. agent forwarding).
*
* @param session The SSH session the agent will be associated with.
*
* @return An allocated ssh agent structure on success, or NULL on error.
*
* @note This function does not start or manage an external agent
* process; it only connects to an already running agent.
*/
struct ssh_agent_struct *ssh_agent_new(struct ssh_session_struct *session);
void ssh_agent_close(struct ssh_agent_struct *agent);
/**
@@ -101,23 +119,75 @@ void ssh_agent_free(struct ssh_agent_struct *agent);
/**
* @brief Check if the ssh agent is running.
*
* @param session The ssh session to check for the agent.
* @param session The SSH session to check for agent availability.
*
* @return 1 if it is running, 0 if not.
* @return 1 if an agent is available, 0 otherwise.
*/
int ssh_agent_is_running(struct ssh_session_struct *session);
uint32_t ssh_agent_get_ident_count(struct ssh_session_struct *session);
ssh_key ssh_agent_get_next_ident(struct ssh_session_struct *session,
char **comment);
/**
* @brief Retrieve the first identity provided by the SSH agent.
*
* @param session The SSH session associated with the agent.
* @param comment Optional pointer to receive the key comment.
*
* @return A public key on success, or NULL if no identities are available.
*
* @note The returned key is owned by the caller and must be freed
* using ssh_key_free().
*/
ssh_key ssh_agent_get_first_ident(struct ssh_session_struct *session,
char **comment);
/**
* @brief Retrieve the next identity provided by the SSH agent.
*
* @param session The SSH session associated with the agent.
* @param comment Optional pointer to receive the key comment.
*
* @return A public key on success, or NULL if no further identities exist.
*
* @note The returned key is owned by the caller and must be freed
* using ssh_key_free().
*/
ssh_key ssh_agent_get_next_ident(struct ssh_session_struct *session,
char **comment);
/**
* @brief Request the SSH agent to sign data using a public key.
*
* Asks the SSH agent to generate a signature over the provided data
* using the specified public key.
*
* @param session The SSH session associated with the agent.
* @param pubkey The public key identifying the signing identity.
* @param data The data to be signed.
*
* @return A newly allocated ssh_string containing the signature on
* success, or NULL on failure.
*
* @note This operation requires that the agent possesses the
* corresponding private key and may prompt the user for
* confirmation depending on agent configuration.
*/
ssh_string ssh_agent_sign_data(ssh_session session,
const ssh_key pubkey,
struct ssh_buffer_struct *data);
/**
* @brief Remove an identity from the SSH agent.
*
* @param session The SSH session.
* @param key The public key to remove.
*
* @return `SSH_OK` on success, `SSH_ERROR` on failure.
*/
int ssh_agent_remove_identity(ssh_session session,
const ssh_key key);
#ifdef __cplusplus
}

View File

@@ -66,16 +66,6 @@ enum ssh_bind_config_opcode_e {
*/
int ssh_bind_config_parse_file(ssh_bind sshbind, const char *filename);
/* @brief Parse configuration string and set the options to the given bind session
*
* @params[in] bind The ssh bind session
* @params[in] input Null terminated string containing the configuration
*
* @returns SSH_OK on successful parsing the configuration string,
* SSH_ERROR on error
*/
int ssh_bind_config_parse_string(ssh_bind bind, const char *input);
#ifdef __cplusplus
}
#endif

View File

@@ -31,6 +31,8 @@
#ifndef _BLF_H_
#define _BLF_H_
#include <stdint.h>
//#include "includes.h"
#if !defined(HAVE_BCRYPT_PBKDF) && !defined(HAVE_BLH_H)

View File

@@ -522,7 +522,7 @@ typedef struct ssh_socket_callbacks_struct *ssh_socket_callbacks;
* verifies that the callback pointer exists
* @param p callback pointer
* @param c callback name
* @returns nonzero if callback can be called
* @return nonzero if callback can be called
*/
#define ssh_callbacks_exists(p,c) (\
(p != NULL) && ( (char *)&((p)-> c) < (char *)(p) + (p)->size ) && \

View File

@@ -9,6 +9,8 @@ Public domain.
#ifndef CHACHA_H
#define CHACHA_H
#include <stdint.h>
struct chacha_ctx {
uint32_t input[16];
};

View File

@@ -29,6 +29,8 @@
#ifndef CHACHA20_POLY1305_H
#define CHACHA20_POLY1305_H
#include <stdint.h>
#define CHACHA20_BLOCKSIZE 64
#define CHACHA20_KEYLEN 32

View File

@@ -54,6 +54,11 @@ int ssh_config_get_yesno(char **str, int notfound);
* be stored or NULL if we do not care about the result.
* @param[in] ignore_port Set to true if we should not attempt to parse
* port number.
* @param[in] strict Set to true to validate hostname against RFC1035
* (for resolving to a real host).
* Set to false to only reject shell metacharacters
* (allowing config aliases with non-RFC1035 chars
* like underscores, resolved later via Hostname).
*
* @returns SSH_OK if the provided string is in format of SSH URI,
* SSH_ERROR on failure
@@ -62,7 +67,8 @@ int ssh_config_parse_uri(const char *tok,
char **username,
char **hostname,
char **port,
bool ignore_port);
bool ignore_port,
bool strict);
/**
* @brief: Parse the ProxyJump configuration line and if parsing,

View File

@@ -67,38 +67,60 @@ class Channel;
*/
#ifndef SSH_NO_CPP_EXCEPTIONS
/** @brief This class describes a SSH Exception object. This object can be thrown
* by several SSH functions that interact with the network, and may fail because of
* socket, protocol or memory errors.
/** @brief This class describes a SSH Exception object. This object can be
* thrown by several SSH functions that interact with the network, and
* may fail because of socket, protocol or memory errors.
*/
class SshException{
public:
SshException(ssh_session csession){
code=ssh_get_error_code(csession);
description=std::string(ssh_get_error(csession));
}
SshException(const SshException &e){
code=e.code;
description=e.description;
}
/** @brief returns the Error code
* @returns SSH_FATAL Fatal error happened (not recoverable)
* @returns SSH_REQUEST_DENIED Request was denied by remote host
* @see ssh_get_error_code
*/
int getCode(){
return code;
}
/** @brief returns the error message of the last exception
* @returns pointer to a c string containing the description of error
* @see ssh_get_error
*/
std::string getError(){
return description;
}
private:
int code;
std::string description;
class SshException {
public:
/** @brief Construct an exception from a libssh session error state.
* Captures the current error code and error string associated with
* the given session.
*
* @param[in] csession libssh session handle used to query error details.
*
* @see ssh_get_error_code
* @see ssh_get_error
*/
SshException(ssh_session csession)
{
code = ssh_get_error_code(csession);
description = std::string(ssh_get_error(csession));
}
/** @brief Copy-construct an exception.
*
* @param[in] e Source exception.
*/
SshException(const SshException &e)
{
code = e.code;
description = e.description;
}
/** @brief returns the Error code
*
* @returns `SSH_FATAL` Fatal error happened (not recoverable)
* @returns `SSH_REQUEST_DENIED` Request was denied by remote host
*
* @see ssh_get_error_code
*/
int getCode()
{
return code;
}
/** @brief returns the error message of the last exception
*
* @returns pointer to a c string containing the description of error
*
* @see ssh_get_error
*/
std::string getError()
{
return description;
}
private:
int code;
std::string description;
};
/** @internal
@@ -134,9 +156,12 @@ public:
c_session=NULL;
}
/** @brief sets an SSH session options
* @param type Type of option
* @param option cstring containing the value of option
*
* @param[in] type Type of option
* @param[in] option cstring containing the value of option
*
* @throws SshException on error
*
* @see ssh_options_set
*/
void_throwable setOption(enum ssh_options_e type, const char *option){
@@ -144,9 +169,12 @@ public:
return_throwable;
}
/** @brief sets an SSH session options
* @param type Type of option
* @param option long integer containing the value of option
*
* @param[in] type Type of option
* @param[in] option long integer containing the value of option
*
* @throws SshException on error
*
* @see ssh_options_set
*/
void_throwable setOption(enum ssh_options_e type, long int option){
@@ -154,9 +182,12 @@ public:
return_throwable;
}
/** @brief sets an SSH session options
* @param type Type of option
* @param option void pointer containing the value of option
*
* @param[in] type Type of option
* @param[in] option void pointer containing the value of option
*
* @throws SshException on error
*
* @see ssh_options_set
*/
void_throwable setOption(enum ssh_options_e type, void *option){
@@ -164,7 +195,9 @@ public:
return_throwable;
}
/** @brief connects to the remote host
*
* @throws SshException on error
*
* @see ssh_connect
*/
void_throwable connect(){
@@ -173,8 +206,11 @@ public:
return_throwable;
}
/** @brief Authenticates automatically using public key
*
* @throws SshException on error
* @returns SSH_AUTH_SUCCESS, SSH_AUTH_PARTIAL, SSH_AUTH_DENIED
*
* @returns `SSH_AUTH_SUCCESS`, `SSH_AUTH_PARTIAL`, `SSH_AUTH_DENIED`
*
* @see ssh_userauth_autopubkey
*/
int userauthPublickeyAuto(void){
@@ -184,9 +220,13 @@ public:
}
/** @brief Authenticates using the "none" method. Prefer using autopubkey if
* possible.
*
* @throws SshException on error
* @returns SSH_AUTH_SUCCESS, SSH_AUTH_PARTIAL, SSH_AUTH_DENIED
*
* @returns `SSH_AUTH_SUCCESS`, `SSH_AUTH_PARTIAL`, `SSH_AUTH_DENIED`
*
* @see ssh_userauth_none
*
* @see Session::userauthAutoPubkey
*/
int userauthNone(){
@@ -198,16 +238,16 @@ public:
/**
* @brief Authenticate through the "keyboard-interactive" method.
*
* @param[in] username 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] username 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.
*
* @throws SshException on error
*
* @returns SSH_AUTH_SUCCESS, SSH_AUTH_PARTIAL, SSH_AUTH_DENIED,
* SSH_AUTH_ERROR, SSH_AUTH_INFO, SSH_AUTH_AGAIN
* @returns `SSH_AUTH_SUCCESS`, `SSH_AUTH_PARTIAL`,
* `SSH_AUTH_DENIED`, `SSH_AUTH_ERROR`, `SSH_AUTH_INFO`,
* `SSH_AUTH_AGAIN`
*
* @see ssh_userauth_kbdint
*/
@@ -218,6 +258,7 @@ public:
}
/** @brief Get the number of prompts (questions) the server has given.
*
* @returns The number of prompts.
* @see ssh_userauth_kbdint_getnprompts
*/
@@ -228,17 +269,16 @@ public:
/**
* @brief Set the answer for a question from a message block.
*
* @param[in] index The index number of the 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.
* @param[in] index The index number of the 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.
*
* @throws SshException on error
*
* @returns 0 on success, < 0 on error
*
* @see ssh_userauth_kbdint_setanswer
*/
int userauthKbdintSetAnswer(unsigned int index, const char *answer)
@@ -248,12 +288,13 @@ public:
return ret;
}
/** @brief Authenticates using the password method.
*
* @param[in] password password to use for authentication
*
* @throws SshException on error
* @returns SSH_AUTH_SUCCESS, SSH_AUTH_PARTIAL, SSH_AUTH_DENIED
* @returns `SSH_AUTH_SUCCESS`, `SSH_AUTH_PARTIAL`, `SSH_AUTH_DENIED`
*
* @see ssh_userauth_password
*/
int userauthPassword(const char *password){
@@ -262,10 +303,14 @@ public:
return ret;
}
/** @brief Try to authenticate using the publickey method.
*
* @param[in] pubkey public key to use for authentication
*
* @throws SshException on error
* @returns SSH_AUTH_SUCCESS if the pubkey is accepted,
* @returns SSH_AUTH_DENIED if the pubkey is denied
*
* @returns `SSH_AUTH_SUCCESS` if the pubkey is accepted,
* @returns `SSH_AUTH_DENIED` if the pubkey is denied
*
* @see ssh_userauth_try_pubkey
*/
int userauthTryPublickey(ssh_key pubkey){
@@ -274,9 +319,12 @@ public:
return ret;
}
/** @brief Authenticates using the publickey method.
*
* @param[in] privkey private key to use for authentication
*
* @throws SshException on error
* @returns SSH_AUTH_SUCCESS, SSH_AUTH_PARTIAL, SSH_AUTH_DENIED
* @returns `SSH_AUTH_SUCCESS`, `SSH_AUTH_PARTIAL`, `SSH_AUTH_DENIED`
*
* @see ssh_userauth_pubkey
*/
int userauthPublickey(ssh_key privkey){
@@ -286,7 +334,9 @@ public:
}
/** @brief Returns the available authentication methods from the server
*
* @throws SshException on error
*
* @returns Bitfield of available methods.
* @see ssh_userauth_list
*/
@@ -302,8 +352,9 @@ public:
ssh_disconnect(c_session);
}
/** @brief Returns the disconnect message from the server, if any
* @returns pointer to the message, or NULL. Do not attempt to free
* the pointer.
*
* @returns pointer to the message, or NULL. Do not attempt to free the
* pointer.
*/
const char *getDisconnectMessage(){
const char *msg=ssh_get_disconnect_message(c_session);
@@ -312,25 +363,30 @@ public:
/** @internal
* @brief gets error message
*/
const char *getError(){
return ssh_get_error(c_session);
const char *getError()
{
return ssh_get_error(c_session);
}
/** @internal
* @brief returns error code
*/
int getErrorCode(){
return ssh_get_error_code(c_session);
int getErrorCode()
{
return ssh_get_error_code(c_session);
}
/** @brief returns the file descriptor used for the communication
*
* @returns the file descriptor
*
* @warning if a proxycommand is used, this function will only return
* one of the two file descriptors being used
* one of the two file descriptors being used.
* @see ssh_get_fd
*/
socket_t getSocket(){
return ssh_get_fd(c_session);
}
/** @brief gets the Issue banner from the ssh server
*
* @returns the issue banner. This is generally a MOTD from server
* @see ssh_get_issue_banner
*/
@@ -344,6 +400,7 @@ public:
return ret;
}
/** @brief returns the OpenSSH version (server) if possible
*
* @returns openssh version code
* @see ssh_get_openssh_version
*/
@@ -351,6 +408,7 @@ public:
return ssh_get_openssh_version(c_session);
}
/** @brief returns the version of the SSH protocol being used
*
* @returns the SSH protocol version
* @see ssh_get_version
*/
@@ -358,9 +416,10 @@ public:
return ssh_get_version(c_session);
}
/** @brief verifies that the server is known
*
* @throws SshException on error
* @returns Integer value depending on the knowledge of the
* server key
*
* @returns Integer value depending on the knowledge of the server key
* @see ssh_session_update_known_hosts
*/
int isServerKnown(){
@@ -377,6 +436,7 @@ public:
}
/** @brief copies options from a session to another
*
* @throws SshException on error
* @see ssh_options_copy
*/
@@ -385,8 +445,11 @@ public:
return_throwable;
}
/** @brief parses a configuration file for options
*
* @throws SshException on error
*
* @param[in] file configuration file name
*
* @see ssh_options_parse_config
*/
void_throwable optionsParseConfig(const char *file){
@@ -399,8 +462,8 @@ public:
void silentDisconnect(){
ssh_silent_disconnect(c_session);
}
/** @brief Writes the known host file with current
* host key
/** @brief Writes the known host file with current host key
*
* @throws SshException on error
* @see ssh_write_knownhost
*/
@@ -411,11 +474,15 @@ public:
}
/** @brief accept an incoming forward connection
*
* @param[in] timeout_ms timeout for waiting, in ms
*
* @returns new Channel pointer on the forward connection
* @returns NULL in case of error
*
* @warning you have to delete this pointer after use
* @see ssh_channel_forward_accept
*
* @see Session::listenForward
*/
inline Channel *acceptForward(int timeout_ms);
@@ -439,6 +506,9 @@ public:
}
protected:
/** @internal
* @brief Underlying libssh session handle.
*/
ssh_session c_session;
private:
@@ -447,8 +517,7 @@ private:
Session& operator=(const Session &);
};
/** @brief the ssh::Channel class describes the state of an SSH
* channel.
/** @brief the ssh::Channel class describes the state of an SSH channel.
* @see ssh_channel
*/
class Channel {
@@ -464,11 +533,15 @@ public:
}
/** @brief accept an incoming X11 connection
*
* @param[in] timeout_ms timeout for waiting, in ms
*
* @returns new Channel pointer on the X11 connection
* @returns NULL in case of error
*
* @warning you have to delete this pointer after use
* @see ssh_channel_accept_x11
*
* @see Channel::requestX11
*/
Channel *acceptX11(int timeout_ms){
@@ -478,8 +551,10 @@ public:
return newchan;
}
/** @brief change the size of a pseudoterminal
*
* @param[in] cols number of columns
* @param[in] rows number of rows
*
* @throws SshException on error
* @see ssh_channel_change_pty_size
*/
@@ -490,6 +565,7 @@ public:
}
/** @brief closes a channel
*
* @throws SshException on error
* @see ssh_channel_close
*/
@@ -536,6 +612,21 @@ public:
bool isOpen(){
return ssh_channel_is_open(channel) != 0;
}
/** @brief Open a TCP forward channel.
*
* @param[in] remotehost Remote host to connect to.
* @param[in] remoteport Remote port to connect to.
* @param[in] sourcehost Source address to report (can be NULL depending on
* server policy).
* @param[in] localport Local port to report (0 lets the server pick if
* applicable).
*
* @returns `SSH_OK` (0) on success.
* @returns `SSH_ERROR` on error (no-exception builds).
*
* @throws SshException on error (exception-enabled builds).
* @see ssh_channel_open_forward
*/
int openForward(const char *remotehost, int remoteport,
const char *sourcehost, int localport=0){
int err=ssh_channel_open_forward(channel,remotehost,remoteport,
@@ -549,20 +640,55 @@ public:
ssh_throw(err);
return_throwable;
}
int poll(bool is_stderr=false){
int err=ssh_channel_poll(channel,is_stderr);
ssh_throw(err);
return err;
/** @brief Poll the channel for available data.
*
* @param[in] is_stderr If true, poll stderr stream; otherwise stdout.
*
* @returns Number of bytes available to read (>= 0).
* @returns `SSH_ERROR` on error (no-exception builds).
*
* @throws SshException on error (exception-enabled builds).
* @see ssh_channel_poll
*/
int poll(bool is_stderr = false)
{
int err = ssh_channel_poll(channel, is_stderr);
ssh_throw(err);
return err;
}
int read(void *dest, size_t count){
int err;
/* handle int overflow */
if(count > 0x7fffffff)
count = 0x7fffffff;
err=ssh_channel_read_timeout(channel,dest,count,false,-1);
ssh_throw(err);
return err;
/** @brief Read data from the channel (blocking).
*
* @param[out] dest Destination buffer.
* @param[in] count Maximum number of bytes to read.
*
* @returns Number of bytes read (>= 0). A return of 0 indicates EOF/no data.
* @returns `SSH_ERROR` on error (no-exception builds).
*
* @throws SshException on error (exception-enabled builds).
* @see ssh_channel_read_timeout
*/
int read(void *dest, size_t count)
{
int err;
if (count > 0x7fffffff)
count = 0x7fffffff;
err = ssh_channel_read_timeout(channel, dest, count, false, -1);
ssh_throw(err);
return err;
}
/** @brief Read data from the channel with a timeout.
*
* @param[out] dest Destination buffer.
* @param[in] count Maximum number of bytes to read.
* @param[in] timeout Timeout in milliseconds. A negative value means
* infinite timeout.
*
* @returns Number of bytes read (>= 0). A return value of 0 indicates EOF.
* @returns `SSH_ERROR` on error (no-exception builds).
*
* @throws SshException on error (exception-enabled builds).
* @see ssh_channel_read_timeout
*/
int read(void *dest, size_t count, int timeout){
int err;
/* handle int overflow */
@@ -572,6 +698,22 @@ public:
ssh_throw(err);
return err;
}
/** @brief Read data from the channel with optional stderr selection and
* timeout.
*
* @param[out] dest Destination buffer.
* @param[in] count Maximum number of bytes to read.
* @param[in] is_stderr If true, read from the stderr stream; otherwise
* read from stdout.
* @param[in] timeout Timeout in milliseconds. A negative value means
* infinite timeout.
*
* @returns Number of bytes read (>= 0). A return value of 0 indicates EOF.
* @returns `SSH_ERROR` on error (no-exception builds).
*
* @throws SshException on error (exception-enabled builds).
* @see ssh_channel_read_timeout
*/
int read(void *dest, size_t count, bool is_stderr=false, int timeout=-1){
int err;
/* handle int overflow */
@@ -581,6 +723,19 @@ public:
ssh_throw(err);
return err;
}
/** @brief Read data from the channel without blocking.
*
* @param[out] dest Destination buffer.
* @param[in] count Maximum number of bytes to read.
* @param[in] is_stderr If true, read from the stderr stream; otherwise read
* from stdout.
*
* @returns Number of bytes read (>= 0). A return of 0 may indicate no data.
* @returns `SSH_ERROR` on error (no-exception builds).
*
* @throws SshException on error (exception-enabled builds).
* @see ssh_channel_read_nonblocking
*/
int readNonblocking(void *dest, size_t count, bool is_stderr=false){
int err;
/* handle int overflow */
@@ -629,6 +784,18 @@ public:
ssh_throw(err);
return_throwable;
}
/** @brief Request X11 forwarding for this channel.
*
* @param[in] single_connection If true, allow only a single X11 connection
* for this channel; further X11 connections are
* refused after the first is accepted.
* @param[in] protocol X11 authentication protocol.
* @param[in] cookie X11 authentication cookie.
* @param[in] screen_number X11 screen number.
*
* @returns `SSH_OK` on success.
* @returns `SSH_ERROR` on error (no-exception builds).
*/
int requestX11(bool single_connection,
const char *protocol, const char *cookie, int screen_number){
int err=ssh_channel_request_x11(channel,single_connection,
@@ -641,11 +808,16 @@ public:
ssh_throw(err);
return_throwable;
}
/** @brief Writes on a channel
* @param data data to write.
* @param len number of bytes to write.
* @param is_stderr write should be done on the stderr channel (server only)
/**
* @brief Writes on a channel
*
* @param[in] data data to write.
* @param[in] len number of bytes to write.
* @param[in] is_stderr write should be done on the stderr channel (server
* only)
*
* @returns number of bytes written
*
* @throws SshException in case of error
* @see ssh_channel_write
* @see ssh_channel_write_stderr
@@ -670,7 +842,13 @@ public:
}
protected:
/** @internal
* @brief Parent session owning this channel.
*/
Session *session;
/** @internal
* @brief Underlying libssh channel handle.
*/
ssh_channel channel;
private:
@@ -683,7 +861,6 @@ private:
Channel &operator=(const Channel &);
};
inline Channel *Session::acceptForward(int timeout_ms){
ssh_channel forward =
ssh_channel_open_forward_port(c_session, timeout_ms, NULL, NULL, NULL);

View File

@@ -5,6 +5,10 @@
#ifndef POLY1305_H
#define POLY1305_H
#include <stddef.h>
#include <stdint.h>
#include "libssh/chacha20-poly1305-common.h"
#ifdef __cplusplus

View File

@@ -9,6 +9,8 @@
#ifndef SC25519_H
#define SC25519_H
#include <stdint.h>
#define sc25519 crypto_sign_ed25519_ref_sc25519
#define shortsc25519 crypto_sign_ed25519_ref_shortsc25519
#define sc25519_from32bytes crypto_sign_ed25519_ref_sc25519_from32bytes

View File

@@ -21,6 +21,9 @@
#ifndef _SCP_H
#define _SCP_H
#include <stddef.h>
#include <stdint.h>
enum ssh_scp_states {
SSH_SCP_NEW, //Data structure just created
SSH_SCP_WRITE_INITED, //Gave our intention to write

View File

@@ -102,12 +102,31 @@ LIBSSH_API int ssh_bind_options_set(ssh_bind sshbind,
LIBSSH_API int ssh_bind_options_parse_config(ssh_bind sshbind,
const char *filename);
LIBSSH_API int ssh_bind_config_parse_string(ssh_bind bind, const char *input);
/**
* @brief Start listening to the socket.
*
* @param ssh_bind_o The ssh server bind to use.
*
* @return 0 on success, < 0 on error.
*
* @warning This function implicitly calls ssh_bind_options_parse_config()
* to process system-wide and user configuration files unless
* configuration processing was already performed explicitly
* by the caller.\n
* As a result, any options previously set (e.g., manually via
* ssh_bind_options_set() or ssh_bind_config_parse_string()) may be
* overridden by values from the configuration files.\n
* To guarantee that explicitly set options take precedence,
* callers of this function should either:
* - call ssh_bind_options_parse_config() themselves before
* setting options, or
* - disable automatic config processing via
* SSH_BIND_OPTIONS_PROCESS_CONFIG (set to false).
*
* @see ssh_bind_options_parse_config()
* @see ssh_bind_options_set()
*/
LIBSSH_API int ssh_bind_listen(ssh_bind ssh_bind_o);

View File

@@ -286,6 +286,9 @@ struct ssh_session_struct {
int control_master;
char *control_path;
int address_family;
char *originalhost; /* user-supplied host for config matching */
bool config_hostname_only; /* config hostname path: update host only,
not originalhost */
} opts;
/* server options */

View File

@@ -177,28 +177,113 @@ struct sftp_status_message_struct {
char *langmsg;
};
/**
* @brief SFTP file attributes structure.
*
* This type represents file attributes. It is used both for
* sending and receiving file attributes from the server.
*
* `flags` determines which of the struct fields are present.
*
* @see sftp_attributes_free()
*/
struct sftp_attributes_struct {
/** File name */
char *name;
char *longname; /* ls -l output on openssh, not reliable else */
/** Extended name i.e output of `ls -l` (requires SFTP v3 with OpenSSH) */
char *longname;
/** Determines which of the struct fields are present */
uint32_t flags;
/** File type */
uint8_t type;
/** File size (requires flag `SSH_FILEXFER_ATTR_SIZE`) */
uint64_t size;
/** User ID (requires SFTP v3 with flag `SSH_FILEXFER_ATTR_UIDGID`) */
uint32_t uid;
/** Group ID (requires SFTP v3 with flag `SSH_FILEXFER_ATTR_UIDGID`) */
uint32_t gid;
char *owner; /* set if openssh and version 4 */
char *group; /* set if openssh and version 4 */
/**
* File owner
* (requires SFTP v4 with flag `SSH_FILEXFER_ATTR_OWNERGROUP`
* or SFTP v3 with OpenSSH)
*/
char *owner;
/**
* File group
* (requires SFTP v4 with flag `SSH_FILEXFER_ATTR_OWNERGROUP`
* or SFTP v3 with OpenSSH)
*/
char *group;
/** File permissions (requires flag `SSH_FILEXFER_ATTR_PERMISSIONS`) */
uint32_t permissions;
/**
* Access time
* (requires SFTP v4 with flag `SSH_FILEXFER_ATTR_ACCESSTIME`)
*/
uint64_t atime64;
/**
* Access time
* (requires SFTP v3 with flag `SSH_FILEXFER_ATTR_ACMODTIME`)
*/
uint32_t atime;
/**
* Access time nanoseconds
* (requires SFTP v4 with flag `SSH_FILEXFER_ATTR_SUBSECOND_TIMES`)
*/
uint32_t atime_nseconds;
/**
* Creation time
* (requires SFTP v4 with flag `SSH_FILEXFER_ATTR_CREATETIME`)
*/
uint64_t createtime;
/**
* Creation time nanoseconds
* (requires SFTP v4 with flag `SSH_FILEXFER_ATTR_SUBSECOND_TIMES`)
*/
uint32_t createtime_nseconds;
/**
* Modification time
* (requires SFTP v4 with flag `SSH_FILEXFER_ATTR_MODIFYTIME`)
*/
uint64_t mtime64;
/**
* Modification time
* (requires SFTP v3 with flag `SSH_FILEXFER_ATTR_ACMODTIME`)
*/
uint32_t mtime;
/**
* Modification time nanoseconds
* (requires SFTP v4 with flag `SSH_FILEXFER_ATTR_SUBSECOND_TIMES`)
*/
uint32_t mtime_nseconds;
/** ACL data (requires SFTP v4 with flag `SSH_FILEXFER_ATTR_ACL`) */
ssh_string acl;
/** Unused */
uint32_t extended_count;
/** Unused */
ssh_string extended_type;
/** Unused */
ssh_string extended_data;
};
@@ -206,27 +291,55 @@ struct sftp_attributes_struct {
* @brief SFTP statvfs structure.
*/
struct sftp_statvfs_struct {
uint64_t f_bsize; /** file system block size */
uint64_t f_frsize; /** fundamental fs block size */
uint64_t f_blocks; /** number of blocks (unit f_frsize) */
uint64_t f_bfree; /** free blocks in file system */
uint64_t f_bavail; /** free blocks for non-root */
uint64_t f_files; /** total file inodes */
uint64_t f_ffree; /** free file inodes */
uint64_t f_favail; /** free file inodes for to non-root */
uint64_t f_fsid; /** file system id */
uint64_t f_flag; /** bit mask of f_flag values */
uint64_t f_namemax; /** maximum filename length */
/** file system block size */
uint64_t f_bsize;
/** fundamental fs block size */
uint64_t f_frsize;
/** number of blocks (unit f_frsize) */
uint64_t f_blocks;
/** free blocks in file system */
uint64_t f_bfree;
/** free blocks for non-root */
uint64_t f_bavail;
/** total file inodes */
uint64_t f_files;
/** free file inodes */
uint64_t f_ffree;
/** free file inodes for non-root */
uint64_t f_favail;
/** file system id */
uint64_t f_fsid;
/** bit mask of f_flag values */
uint64_t f_flag;
/** maximum filename length */
uint64_t f_namemax;
};
/**
* @brief SFTP limits structure.
*/
struct sftp_limits_struct {
uint64_t max_packet_length; /** maximum number of bytes in a single sftp packet */
uint64_t max_read_length; /** maximum length in a SSH_FXP_READ packet */
uint64_t max_write_length; /** maximum length in a SSH_FXP_WRITE packet */
uint64_t max_open_handles; /** maximum number of active handles allowed by server */
/** maximum number of bytes in a single sftp packet */
uint64_t max_packet_length;
/** maximum length in a SSH_FXP_READ packet */
uint64_t max_read_length;
/** maximum length in a SSH_FXP_WRITE packet */
uint64_t max_write_length;
/** maximum number of active handles allowed by server */
uint64_t max_open_handles;
};
/**

View File

@@ -635,3 +635,91 @@ ssh_string ssh_agent_sign_data(ssh_session session,
return sig_blob;
}
int ssh_agent_remove_identity(ssh_session session,
const ssh_key key)
{
ssh_buffer request = NULL;
ssh_buffer reply = NULL;
ssh_string key_blob = NULL;
uint8_t type = 0;
int rc = SSH_ERROR;
if (session == NULL || key == NULL) {
return SSH_ERROR;
}
if (session->agent == NULL) {
ssh_set_error(session,
SSH_FATAL,
"No agent connection available");
return SSH_ERROR;
}
/* Connect to the agent if not already connected */
if (!ssh_socket_is_open(session->agent->sock)) {
if (agent_connect(session) < 0) {
ssh_set_error(session,
SSH_FATAL,
"Could not connect to SSH agent");
return SSH_ERROR;
}
}
request = ssh_buffer_new();
if (request == NULL) {
ssh_set_error_oom(session);
goto fail;
}
if (ssh_buffer_add_u8(request, SSH2_AGENTC_REMOVE_IDENTITY) < 0) {
ssh_set_error_oom(session);
goto fail;
}
if (ssh_pki_export_pubkey_blob(key, &key_blob) < 0) {
ssh_set_error(session, SSH_FATAL, "Failed to export public key blob");
goto fail;
}
if (ssh_buffer_add_ssh_string(request, key_blob) < 0) {
ssh_set_error_oom(session);
goto fail;
}
reply = ssh_buffer_new();
if (reply == NULL) {
ssh_set_error_oom(session);
goto fail;
}
if (agent_talk(session, request, reply) < 0) {
goto fail;
}
if (ssh_buffer_get_u8(reply, &type) != sizeof(uint8_t)) {
ssh_set_error(session,
SSH_FATAL,
"Failed to read agent reply type");
goto fail;
}
if (agent_failed(type)) {
SSH_LOG(SSH_LOG_DEBUG, "Agent reports failure removing identity");
goto fail;
} else if (type != SSH_AGENT_SUCCESS) {
ssh_set_error(session,
SSH_FATAL,
"Agent refused to remove identity: reply type %u",
type);
goto fail;
}
rc = SSH_OK;
fail:
SSH_STRING_FREE(key_blob);
SSH_BUFFER_FREE(request);
SSH_BUFFER_FREE(reply);
return rc;
}

View File

@@ -1397,7 +1397,7 @@ int ssh_userauth_publickey_auto(ssh_session session,
return SSH_AUTH_ERROR;
}
SSH_LOG(SSH_LOG_INFO,
SSH_LOG(SSH_LOG_DEBUG,
"Starting authentication as a user %s",
username ? username : session->opts.username);
@@ -1835,6 +1835,14 @@ int ssh_userauth_agent_pubkey(ssh_session session,
return rc;
}
/**
* @internal
*
* @brief Allocates memory for keyboard interactive auth structure.
*
* @return A newly allocated ssh_kbdint structure `kbd` on success, NULL on failure.
* The caller is responsible for freeing allocated memory.
*/
ssh_kbdint ssh_kbdint_new(void)
{
ssh_kbdint kbd;
@@ -1847,7 +1855,11 @@ ssh_kbdint ssh_kbdint_new(void)
return kbd;
}
/**
* @brief Deallocate memory for keyboard interactive auth structure.
*
* @param[in] kbd The keyboard interactive structure to free.
*/
void ssh_kbdint_free(ssh_kbdint kbd)
{
size_t i, n;
@@ -1885,6 +1897,14 @@ void ssh_kbdint_free(ssh_kbdint kbd)
SAFE_FREE(kbd);
}
/**
* @brief Clean a keyboard interactive auth structure.
*
* Clears structure's fields and resets nanswers and nprompts to 0, allowing
* reuse.
*
* @param[in] kbd The keyboard interactive struct to clean
*/
void ssh_kbdint_clean(ssh_kbdint kbd)
{
size_t i, n;

View File

@@ -327,7 +327,7 @@ static int ssh_bind_poll_callback(ssh_poll_handle sshpoll, socket_t fd, int reve
/** @internal
* @brief returns the current poll handle, or creates it
* @param sshbind the ssh_bind object
* @returns a ssh_poll handle suitable for operation
* @return a ssh_poll handle suitable for operation
*/
ssh_poll_handle ssh_bind_get_poll(ssh_bind sshbind)
{

View File

@@ -676,12 +676,21 @@ int ssh_bind_config_parse_file(ssh_bind bind, const char *filename)
return 0;
}
/* @brief Parse configuration string and set the options to the given bind session
/**
* @brief Parse configuration string and set the options to the given bind
* session
*
* @params[in] bind The ssh bind session
* @params[in] input Null terminated string containing the configuration
* @param[in] bind The ssh bind session
* @param[in] input Null terminated string containing the configuration
*
* @returns SSH_OK on successful parsing the configuration string,
* @warning Options set via this function may be overridden if a configuration
* file is parsed afterwards (e.g., by an implicit call to
* ssh_bind_options_parse_config() inside ssh_bind_listen(), or by a
* manual call to the same function) and contains the same options.\n
* It is the callers responsibility to ensure the correct order of
* API calls if explicit options must take precedence.
*
* @return SSH_OK on successful parsing the configuration string,
* SSH_ERROR on error
*/
int ssh_bind_config_parse_string(ssh_bind bind, const char *input)
@@ -713,21 +722,29 @@ int ssh_bind_config_parse_string(ssh_bind bind, const char *input)
}
if (c == NULL) {
/* should not happen, would mean a string without trailing '\0' */
SSH_LOG(SSH_LOG_WARN, "No trailing '\\0' in config string");
ssh_set_error(bind,
SSH_FATAL,
"No trailing '\\0' in config string");
return SSH_ERROR;
}
line_len = c - line_start;
if (line_len > MAX_LINE_SIZE - 1) {
SSH_LOG(SSH_LOG_WARN,
"Line %u too long: %zu characters",
line_num,
line_len);
ssh_set_error(bind,
SSH_FATAL,
"Line %u too long: %zu characters",
line_num,
line_len);
return SSH_ERROR;
}
memcpy(line, line_start, line_len);
line[line_len] = '\0';
SSH_LOG(SSH_LOG_DEBUG, "Line %u: %s", line_num, line);
rv = ssh_bind_config_parse_line(bind, line, line_num, &parser_flags, seen, 0);
rv = ssh_bind_config_parse_line(bind,
line,
line_num,
&parser_flags,
seen,
0);
if (rv < 0) {
return SSH_ERROR;
}

View File

@@ -921,18 +921,10 @@ static int ssh_buffer_pack_allocate_va(struct ssh_buffer_struct *buffer,
va_arg(ap, void *);
count++; /* increase argument count */
break;
case 'F':
case 'B':
b = va_arg(ap, bignum);
if (*p == 'F') {
/* For padded bignum, we know the exact length */
len = va_arg(ap, size_t);
count++; /* increase argument count */
needed_size += sizeof(uint32_t) + len;
} else {
/* The bignum bytes + 1 for possible padding */
needed_size += sizeof(uint32_t) + bignum_num_bytes(b) + 1;
}
/* The bignum bytes + 1 for possible padding */
needed_size += sizeof(uint32_t) + bignum_num_bytes(b) + 1;
break;
case 't':
cstring = va_arg(ap, char *);
@@ -1062,16 +1054,9 @@ ssh_buffer_pack_va(struct ssh_buffer_struct *buffer,
rc = ssh_buffer_add_data(buffer, o.data, (uint32_t)len);
o.data = NULL;
break;
case 'F':
case 'B':
b = va_arg(ap, bignum);
if (*p == 'F') {
len = va_arg(ap, size_t);
count++; /* increase argument count */
o.string = ssh_make_padded_bignum_string(b, len);
} else {
o.string = ssh_make_bignum_string(b);
}
o.string = ssh_make_bignum_string(b);
if(o.string == NULL){
rc = SSH_ERROR;
break;
@@ -1127,8 +1112,6 @@ ssh_buffer_pack_va(struct ssh_buffer_struct *buffer,
* 'P': size_t, void * (len of data, pointer to data)
* only pushes data.
* 'B': bignum (pushed as SSH string)
* 'F': bignum, size_t (bignum, padded to fixed length,
* pushed as SSH string)
* @returns SSH_OK on success
* SSH_ERROR on error
* @warning when using 'P' with a constant size (e.g. 8), do not

View File

@@ -3517,7 +3517,7 @@ int ssh_channel_get_exit_state(ssh_channel channel,
*pexit_signal = NULL;
if (channel->exit.signal != NULL) {
*pexit_signal = strdup(channel->exit.signal);
if (pexit_signal == NULL) {
if (*pexit_signal == NULL) {
ssh_set_error_oom(session);
return SSH_ERROR;
}

View File

@@ -582,9 +582,8 @@ int ssh_connect(ssh_session session)
session->client = 1;
if (session->opts.fd == SSH_INVALID_SOCKET &&
session->opts.host == NULL &&
session->opts.ProxyCommand == NULL)
{
session->opts.originalhost == NULL &&
session->opts.ProxyCommand == NULL) {
ssh_set_error(session, SSH_FATAL, "Hostname required");
return SSH_ERROR;
}

View File

@@ -258,7 +258,9 @@ local_parse_file(ssh_session session,
f = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE);
if (f == NULL) {
/* The underlying function logs the reasons */
SSH_LOG(SSH_LOG_RARE,
"Failed to open included configuration file %s",
filename);
return;
}
@@ -542,6 +544,7 @@ ssh_config_parse_proxy_jump(ssh_session session, const char *s, bool do_parsing)
&jump_host->username,
&jump_host->hostname,
&port,
false,
false);
if (rv != SSH_OK) {
ssh_set_error_invalid(session);
@@ -564,7 +567,12 @@ ssh_config_parse_proxy_jump(ssh_session session, const char *s, bool do_parsing)
}
} else if (parse_entry) {
/* We actually care only about the first item */
rv = ssh_config_parse_uri(cp, &username, &hostname, &port, false);
rv = ssh_config_parse_uri(cp,
&username,
&hostname,
&port,
false,
false);
if (rv != SSH_OK) {
ssh_set_error_invalid(session);
goto out;
@@ -580,7 +588,7 @@ ssh_config_parse_proxy_jump(ssh_session session, const char *s, bool do_parsing)
}
} else {
/* The rest is just sanity-checked to avoid failures later */
rv = ssh_config_parse_uri(cp, NULL, NULL, NULL, false);
rv = ssh_config_parse_uri(cp, NULL, NULL, NULL, false, false);
if (rv != SSH_OK) {
ssh_set_error_invalid(session);
goto out;
@@ -1038,20 +1046,20 @@ static int ssh_config_parse_line_internal(ssh_session session,
break;
case MATCH_ORIGINALHOST:
/* Skip one argument */
/* Here we match only one argument */
p = ssh_config_get_str_tok(&s, NULL);
if (p == NULL || p[0] == '\0') {
SSH_LOG(SSH_LOG_TRACE, "line %d: Match keyword "
"'%s' requires argument", count, p2);
ssh_set_error(session,
SSH_FATAL,
"line %d: ERROR - Match originalhost keyword "
"requires argument",
count);
SAFE_FREE(x);
return -1;
}
result &=
ssh_config_match(session->opts.originalhost, p, negate);
args++;
SSH_LOG(SSH_LOG_TRACE,
"line %d: Unsupported Match keyword '%s', ignoring",
count,
p2);
result = 0;
break;
case MATCH_HOST:
@@ -1064,7 +1072,11 @@ static int ssh_config_parse_line_internal(ssh_session session,
SAFE_FREE(x);
return -1;
}
result &= ssh_config_match(session->opts.host, p, negate);
result &= ssh_config_match(session->opts.host
? session->opts.host
: session->opts.originalhost,
p,
negate);
args++;
break;
@@ -1152,7 +1164,9 @@ static int ssh_config_parse_line_internal(ssh_session session,
int ok = 0, result = -1;
*parsing = 0;
lowerhost = (session->opts.host) ? ssh_lowercase(session->opts.host) : NULL;
lowerhost = (session->opts.originalhost)
? ssh_lowercase(session->opts.originalhost)
: NULL;
for (p = ssh_config_get_str_tok(&s, NULL);
p != NULL && p[0] != '\0';
p = ssh_config_get_str_tok(&s, NULL)) {
@@ -1179,7 +1193,9 @@ static int ssh_config_parse_line_internal(ssh_session session,
if (z == NULL) {
z = strdup(p);
}
session->opts.config_hostname_only = true;
ssh_options_set(session, SSH_OPTIONS_HOST, z);
session->opts.config_hostname_only = false;
free(z);
}
break;

View File

@@ -172,7 +172,8 @@ int ssh_config_parse_uri(const char *tok,
char **username,
char **hostname,
char **port,
bool ignore_port)
bool ignore_port,
bool strict)
{
char *endp = NULL;
long port_n;
@@ -243,13 +244,31 @@ int ssh_config_parse_uri(const char *tok,
if (*hostname == NULL) {
goto error;
}
/* if not an ip, check syntax */
rc = ssh_is_ipaddr(*hostname);
if (rc == 0) {
rc = ssh_check_hostname_syntax(*hostname);
if (rc != SSH_OK) {
if (strict) {
/* if not an ip, check syntax */
rc = ssh_is_ipaddr(*hostname);
if (rc == 0) {
rc = ssh_check_hostname_syntax(*hostname);
if (rc != SSH_OK) {
goto error;
}
}
} else {
/* Reject shell metacharacters to allow config aliases with
* non-RFC1035 chars (e.g. %, _). Modeled on OpenSSH's
* valid_hostname() in ssh.c. */
const char *c = NULL;
if ((*hostname)[0] == '-') {
goto error;
}
for (c = *hostname; *c != '\0'; c++) {
char *is_meta = strchr("'`\"$\\;&<>|(){},", *c);
int is_space = isspace((unsigned char)*c);
int is_ctrl = iscntrl((unsigned char)*c);
if (is_meta != NULL || is_space || is_ctrl) {
goto error;
}
}
}
}
/* Skip also the closing bracket */

View File

@@ -243,6 +243,7 @@ ssh_gssapi_handle_userauth(ssh_session session, const char *user,
/* Get the server supported oids */
rc = ssh_gssapi_server_oids(&supported);
if (rc != SSH_OK) {
gss_release_oid_set(&min_stat, &both_supported);
return SSH_ERROR;
}
@@ -730,7 +731,8 @@ int ssh_gssapi_check_client_config(ssh_session session)
gssapi = calloc(1, sizeof(struct ssh_gssapi_struct));
if (gssapi == NULL) {
ssh_set_error_oom(session);
return SSH_ERROR;
ret = SSH_ERROR;
break;
}
gssapi->server_creds = GSS_C_NO_CREDENTIAL;
gssapi->client_creds = GSS_C_NO_CREDENTIAL;
@@ -819,6 +821,11 @@ int ssh_gssapi_check_client_config(ssh_session session)
gss_release_buffer(&min_stat, &output_token);
gss_delete_sec_context(&min_stat, &gssapi->ctx, GSS_C_NO_BUFFER);
if (client_id != GSS_C_NO_NAME) {
gss_release_name(&min_stat, &client_id);
client_id = GSS_C_NO_NAME;
}
SAFE_FREE(gssapi->canonic_user);
SAFE_FREE(gssapi);

View File

@@ -152,7 +152,7 @@ static int derive_hybrid_secret(ssh_session session,
rc = ssh_buffer_pack(combined_secret,
"PP",
MLKEM_SHARED_SECRET_SIZE,
(size_t)MLKEM_SHARED_SECRET_SIZE,
mlkem_shared_secret,
ssh_string_len(ecdh_shared_secret),
ssh_string_data(ecdh_shared_secret));
@@ -244,7 +244,7 @@ int ssh_client_hybrid_mlkem_init(ssh_session session)
"PP",
ssh_string_len(crypto->mlkem_client_pubkey),
ssh_string_data(crypto->mlkem_client_pubkey),
CURVE25519_PUBKEY_SIZE,
(size_t)CURVE25519_PUBKEY_SIZE,
crypto->curve25519_client_pubkey);
break;
case SSH_KEX_MLKEM768NISTP256_SHA256:
@@ -768,7 +768,7 @@ static SSH_PACKET_CALLBACK(ssh_packet_server_hybrid_mlkem_init)
"PP",
ssh_string_len(crypto->mlkem_ciphertext),
ssh_string_data(crypto->mlkem_ciphertext),
CURVE25519_PUBKEY_SIZE,
(size_t)CURVE25519_PUBKEY_SIZE,
crypto->curve25519_server_pubkey);
break;
case SSH_KEX_MLKEM768NISTP256_SHA256:

View File

@@ -591,6 +591,7 @@ int ssh_server_gss_kex_process_init(ssh_session session, ssh_buffer packet)
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");
gss_release_buffer(&min_stat, &output_token);
goto error;
}
SSH_LOG(SSH_LOG_DEBUG, "token accepted");
@@ -607,6 +608,7 @@ int ssh_server_gss_kex_process_init(ssh_session session, ssh_buffer packet)
"creating mic failed",
maj_stat,
min_stat);
gss_release_buffer(&min_stat, &output_token);
goto error;
}
@@ -621,15 +623,14 @@ int ssh_server_gss_kex_process_init(ssh_session session, ssh_buffer packet)
output_token.length,
(size_t)output_token.length,
output_token.value);
gss_release_buffer(&min_stat, &output_token);
gss_release_buffer(&min_stat, &mic);
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;

View File

@@ -1688,11 +1688,6 @@ int ssh_make_sessionid(ssh_session session)
switch (session->next_crypto->kex_type) {
case SSH_KEX_SNTRUP761X25519_SHA512:
case SSH_KEX_SNTRUP761X25519_SHA512_OPENSSH_COM:
rc = ssh_buffer_pack(buf,
"F",
session->next_crypto->shared_secret,
SHA512_DIGEST_LEN);
break;
case SSH_KEX_MLKEM768X25519_SHA256:
case SSH_KEX_MLKEM768NISTP256_SHA256:
#ifdef HAVE_MLKEM1024
@@ -1919,9 +1914,6 @@ int ssh_generate_session_keys(ssh_session session)
switch (session->next_crypto->kex_type) {
case SSH_KEX_SNTRUP761X25519_SHA512:
case SSH_KEX_SNTRUP761X25519_SHA512_OPENSSH_COM:
k_string = ssh_make_padded_bignum_string(crypto->shared_secret,
crypto->digest_len);
break;
case SSH_KEX_MLKEM768X25519_SHA256:
case SSH_KEX_MLKEM768NISTP256_SHA256:
#ifdef HAVE_MLKEM1024

View File

@@ -507,3 +507,9 @@ LIBSSH_4_11_0 # Released
sshsig_verify;
} LIBSSH_4_10_0;
LIBSSH_AFTER_4_11_0
{
global:
ssh_bind_config_parse_string;
} LIBSSH_4_11_0;

View File

@@ -208,6 +208,12 @@ struct tm *ssh_localtime(const time_t *timer, struct tm *result)
return result;
}
/**
* @brief Get username from the calling process.
*
* @return An allocated string with the user on success, NULL on failure. The
* caller is responsible for freeing returned string.
*/
char *ssh_get_local_username(void)
{
DWORD size = 0;
@@ -357,6 +363,12 @@ int ssh_dir_writeable(const char *path)
return 0;
}
/**
* @brief Get username from the calling process.
*
* @return An allocated string with the name on success, NULL on failure. The
* caller is responsible for freeing returned string.
*/
char *ssh_get_local_username(void)
{
struct passwd pwd;
@@ -1432,6 +1444,8 @@ char *ssh_path_expand_escape(ssh_session session, const char *s)
case 'h':
if (session->opts.host) {
x = strdup(session->opts.host);
} else if (session->opts.originalhost) {
x = strdup(session->opts.originalhost);
} else {
ssh_set_error(session, SSH_FATAL, "Cannot expand host");
free(buf);
@@ -2454,7 +2468,7 @@ FILE *ssh_strict_fopen(const char *filename, size_t max_file_size)
/* open first to avoid TOCTOU */
fd = open(filename, O_RDONLY);
if (fd == -1) {
SSH_LOG(SSH_LOG_RARE,
SSH_LOG(SSH_LOG_TRACE,
"Failed to open a file %s for reading: %s",
filename,
ssh_strerror(errno, err_msg, SSH_ERRNO_MSG_MAX));
@@ -2464,7 +2478,7 @@ FILE *ssh_strict_fopen(const char *filename, size_t max_file_size)
/* Check the file is sensible for a configuration file */
r = fstat(fd, &sb);
if (r != 0) {
SSH_LOG(SSH_LOG_RARE,
SSH_LOG(SSH_LOG_TRACE,
"Failed to stat %s: %s",
filename,
ssh_strerror(errno, err_msg, SSH_ERRNO_MSG_MAX));
@@ -2472,7 +2486,7 @@ FILE *ssh_strict_fopen(const char *filename, size_t max_file_size)
return NULL;
}
if ((sb.st_mode & S_IFMT) != S_IFREG) {
SSH_LOG(SSH_LOG_RARE,
SSH_LOG(SSH_LOG_TRACE,
"The file %s is not a regular file: skipping",
filename);
close(fd);
@@ -2480,7 +2494,7 @@ FILE *ssh_strict_fopen(const char *filename, size_t max_file_size)
}
if ((size_t)sb.st_size > max_file_size) {
SSH_LOG(SSH_LOG_RARE,
SSH_LOG(SSH_LOG_TRACE,
"The file %s is too large (%jd MB > %zu MB): skipping",
filename,
(intmax_t)sb.st_size / 1024 / 1024,
@@ -2491,7 +2505,7 @@ FILE *ssh_strict_fopen(const char *filename, size_t max_file_size)
f = fdopen(fd, "r");
if (f == NULL) {
SSH_LOG(SSH_LOG_RARE,
SSH_LOG(SSH_LOG_TRACE,
"Failed to open a file %s for reading: %s",
filename,
ssh_strerror(r, err_msg, SSH_ERRNO_MSG_MAX));

View File

@@ -23,6 +23,7 @@
*/
#include "config.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -106,6 +107,14 @@ int ssh_options_copy(ssh_session src, ssh_session *dest)
}
}
if (src->opts.originalhost != NULL) {
new->opts.originalhost = strdup(src->opts.originalhost);
if (new->opts.originalhost == NULL) {
ssh_free(new);
return -1;
}
}
if (src->opts.bindaddr != NULL) {
new->opts.bindaddr = strdup(src->opts.bindaddr);
if (new->opts.bindaddr == NULL) {
@@ -369,8 +378,8 @@ int ssh_options_set_algo(ssh_session session,
* default ssh directory.\n
* \n
* The ssh directory is used for files like known_hosts
* and identity (private and public key). It may include
* "%s" which will be replaced by the user home
* and identity (private and public key). It may start
* with ~ which will be replaced by the user home
* directory.
*
* - SSH_OPTIONS_KNOWNHOSTS:
@@ -718,18 +727,52 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type,
return -1;
} else {
char *username = NULL, *hostname = NULL;
rc = ssh_config_parse_uri(value, &username, &hostname, NULL, true);
if (rc != SSH_OK) {
char *strict_hostname = NULL;
/* Non-strict parse: reject shell metacharacters */
rc = ssh_config_parse_uri(value,
&username,
&hostname,
NULL,
true,
false);
if (rc != SSH_OK || hostname == NULL) {
SAFE_FREE(username);
SAFE_FREE(hostname);
ssh_set_error_invalid(session);
return -1;
}
/* Non-strict passed: set username and originalhost */
if (username != NULL) {
SAFE_FREE(session->opts.username);
session->opts.username = username;
}
if (hostname != NULL) {
if (!session->opts.config_hostname_only) {
SAFE_FREE(session->opts.originalhost);
session->opts.originalhost = hostname;
} else {
SAFE_FREE(hostname);
}
/* Strict parse: set host only if valid hostname or IP */
rc = ssh_config_parse_uri(value,
NULL,
&strict_hostname,
NULL,
true,
true);
if (rc != SSH_OK || strict_hostname == NULL) {
SAFE_FREE(session->opts.host);
session->opts.host = hostname;
SAFE_FREE(strict_hostname);
if (session->opts.config_hostname_only) {
/* Config path: Hostname must be valid */
ssh_set_error_invalid(session);
return -1;
}
} else {
SAFE_FREE(session->opts.host);
session->opts.host = strict_hostname;
}
}
break;
@@ -1646,7 +1689,8 @@ int ssh_options_get(ssh_session session, enum ssh_options_e type, char** value)
switch(type)
{
case SSH_OPTIONS_HOST:
src = session->opts.host;
src = session->opts.host ? session->opts.host
: session->opts.originalhost;
break;
case SSH_OPTIONS_USER:
@@ -1980,7 +2024,7 @@ int ssh_options_parse_config(ssh_session session, const char *filename)
if (session == NULL) {
return -1;
}
if (session->opts.host == NULL) {
if (session->opts.originalhost == NULL) {
ssh_set_error_invalid(session);
return -1;
}
@@ -2401,6 +2445,15 @@ static int ssh_bind_set_algo(ssh_bind sshbind,
* not a pointer when it should have been a pointer, or if
* its a pointer to a pointer when it should have just been
* a pointer), then the behaviour is undefined.
*
* @warning Options set via this function may be overridden if a
* configuration file is parsed afterwards (e.g., by an
* implicit call to ssh_bind_options_parse_config() inside
* ssh_bind_listen(), or by a manual call to the same
* function) and contains the same options.\n
* It is the callers responsibility to ensure the correct
* order of API calls if explicit options must take
* precedence.
*/
int
ssh_bind_options_set(ssh_bind sshbind,

View File

@@ -406,6 +406,7 @@ void ssh_free(ssh_session session)
SAFE_FREE(session->opts.bindaddr);
SAFE_FREE(session->opts.username);
SAFE_FREE(session->opts.host);
SAFE_FREE(session->opts.originalhost);
SAFE_FREE(session->opts.homedir);
SAFE_FREE(session->opts.sshdir);
SAFE_FREE(session->opts.knownhosts);

View File

@@ -489,7 +489,10 @@ sftp_reply_name(sftp_client_message msg, const char *name, sftp_attributes attr)
return -1;
}
SSH_LOG(SSH_LOG_PROTOCOL, "Sending name %s", ssh_string_get_char(file));
SSH_LOG(SSH_LOG_PROTOCOL,
"Sending name %s, ID %" PRIu32,
ssh_string_get_char(file),
msg->id);
if (ssh_buffer_add_u32(out, msg->id) < 0 ||
ssh_buffer_add_u32(out, htonl(1)) < 0 ||
@@ -532,6 +535,7 @@ int sftp_reply_handle(sftp_client_message msg, ssh_string handle)
ssh_log_hexdump("Sending handle:",
(const unsigned char *)ssh_string_get_char(handle),
ssh_string_len(handle));
SSH_LOG(SSH_LOG_PROTOCOL, "packet ID %" PRIu32, msg->id);
if (ssh_buffer_add_u32(out, msg->id) < 0 ||
ssh_buffer_add_ssh_string(out, handle) < 0 ||
@@ -565,7 +569,7 @@ int sftp_reply_attr(sftp_client_message msg, sftp_attributes attr)
return -1;
}
SSH_LOG(SSH_LOG_PROTOCOL, "Sending attr");
SSH_LOG(SSH_LOG_PROTOCOL, "Sending attr, ID %" PRIu32, msg->id);
if (ssh_buffer_add_u32(out, msg->id) < 0 ||
buffer_add_attributes(out, attr) < 0 ||
@@ -653,7 +657,10 @@ int sftp_reply_names(sftp_client_message msg)
return -1;
}
SSH_LOG(SSH_LOG_PROTOCOL, "Sending %d names", msg->attr_num);
SSH_LOG(SSH_LOG_PROTOCOL,
"Sending %d names, ID %" PRIu32,
msg->attr_num,
msg->id);
if (ssh_buffer_add_u32(out, msg->id) < 0 ||
ssh_buffer_add_u32(out, htonl(msg->attr_num)) < 0 ||
@@ -705,8 +712,11 @@ sftp_reply_status(sftp_client_message msg, uint32_t status, const char *message)
return -1;
}
SSH_LOG(SSH_LOG_PROTOCOL, "Sending status %d, message: %s", status,
ssh_string_get_char(s));
SSH_LOG(SSH_LOG_PROTOCOL,
"Sending status %d, message: %s, ID %" PRIu32,
status,
ssh_string_get_char(s),
msg->id);
if (ssh_buffer_add_u32(out, msg->id) < 0 ||
ssh_buffer_add_u32(out, htonl(status)) < 0 ||
@@ -745,7 +755,10 @@ int sftp_reply_data(sftp_client_message msg, const void *data, int len)
return -1;
}
SSH_LOG(SSH_LOG_PROTOCOL, "Sending data, length: %d", len);
SSH_LOG(SSH_LOG_PROTOCOL,
"Sending data, length: %d, ID %" PRIu32,
len,
msg->id);
if (ssh_buffer_add_u32(out, msg->id) < 0 ||
ssh_buffer_add_u32(out, ntohl(len)) < 0 ||
@@ -780,7 +793,7 @@ sftp_reply_statvfs(sftp_client_message msg, sftp_statvfs_t st)
return -1;
}
SSH_LOG(SSH_LOG_PROTOCOL, "Sending statvfs reply");
SSH_LOG(SSH_LOG_PROTOCOL, "Sending statvfs reply, ID %" PRIu32, msg->id);
if (ssh_buffer_add_u32(out, msg->id) < 0 ||
ssh_buffer_add_u64(out, ntohll(st->f_bsize)) < 0 ||
@@ -810,7 +823,9 @@ int sftp_reply_version(sftp_client_message client_msg)
ssh_buffer reply;
int rc;
SSH_LOG(SSH_LOG_PROTOCOL, "Sending version packet");
SSH_LOG(SSH_LOG_PROTOCOL,
"Sending version packet, ID %" PRIu32,
client_msg->id);
version = sftp->client_version;
reply = ssh_buffer_new();
@@ -1183,6 +1198,10 @@ process_read(sftp_client_message client_msg)
ssh_log_hexdump("Processing read: handle:",
(const unsigned char *)ssh_string_get_char(handle),
ssh_string_len(handle));
SSH_LOG(SSH_LOG_PROTOCOL,
"Offset %" PRIu64", length %" PRIu32,
client_msg->offset,
client_msg->len);
h = sftp_handle(sftp, handle);
if (h != NULL && h->type == SFTP_FILE_HANDLE) {
@@ -1241,6 +1260,10 @@ process_write(sftp_client_message client_msg)
ssh_log_hexdump("Processing write: handle",
(const unsigned char *)ssh_string_get_char(handle),
ssh_string_len(handle));
SSH_LOG(SSH_LOG_PROTOCOL,
"Offset %" PRIu64", length %" PRIu32,
client_msg->offset,
client_msg->len);
h = sftp_handle(sftp, handle);
if (h != NULL && h->type == SFTP_FILE_HANDLE) {
@@ -1343,6 +1366,15 @@ process_opendir(sftp_client_message client_msg)
}
h->dirp = dir;
h->name = strdup(dir_name);
if (h->name == NULL) {
free(h);
closedir(dir);
SSH_LOG(SSH_LOG_PROTOCOL, "failed to duplicate directory name");
sftp_reply_status(client_msg,
SSH_FX_FAILURE,
"Failed to allocate new handle");
return SSH_ERROR;
}
h->type = SFTP_DIR_HANDLE;
handle_s = sftp_handle_alloc(client_msg->sftp, h);
@@ -1350,6 +1382,7 @@ process_opendir(sftp_client_message client_msg)
sftp_reply_handle(client_msg, handle_s);
ssh_string_free(handle_s);
} else {
SAFE_FREE(h->name);
free(h);
closedir(dir);
sftp_reply_status(client_msg, SSH_FX_FAILURE, "No handle available");
@@ -1958,7 +1991,10 @@ dispatch_sftp_request(sftp_client_message sftp_msg)
sftp_server_message_callback handler = NULL;
uint8_t type = sftp_client_message_get_type(sftp_msg);
SSH_LOG(SSH_LOG_PROTOCOL, "processing request type: %u", type);
SSH_LOG(SSH_LOG_PROTOCOL,
"processing request type: %" PRIu8 ", ID %" PRIu32,
type,
sftp_msg->id);
for (int i = 0; message_handlers[i].cb != NULL; i++) {
if (type == message_handlers[i].type) {

View File

@@ -28,7 +28,6 @@
#include "libssh/sntrup761.h"
#ifdef HAVE_SNTRUP761
#include "libssh/bignum.h"
#include "libssh/buffer.h"
#include "libssh/crypto.h"
#include "libssh/dh.h"
@@ -141,7 +140,7 @@ static int ssh_sntrup761x25519_build_k(ssh_session session)
{
unsigned char ssk[SNTRUP761_SIZE + CURVE25519_PUBKEY_SIZE];
unsigned char *k = ssk + SNTRUP761_SIZE;
unsigned char hss[SHA512_DIGEST_LEN];
void *shared_secret_data = NULL;
int rc;
rc = ssh_curve25519_create_k(session, k);
@@ -216,22 +215,27 @@ static int ssh_sntrup761x25519_build_k(ssh_session session)
ssh_log_hexdump("kem key", ssk, SNTRUP761_SIZE);
#endif
sha512(ssk, sizeof ssk, hss);
bignum_bin2bn(hss, sizeof hss, &session->next_crypto->shared_secret);
if (session->next_crypto->shared_secret == NULL) {
ssh_string_burn(session->next_crypto->hybrid_shared_secret);
ssh_string_free(session->next_crypto->hybrid_shared_secret);
session->next_crypto->hybrid_shared_secret =
ssh_string_new(SHA512_DIGEST_LEN);
if (session->next_crypto->hybrid_shared_secret == NULL) {
ssh_set_error_oom(session);
rc = SSH_ERROR;
goto cleanup;
}
shared_secret_data =
ssh_string_data(session->next_crypto->hybrid_shared_secret);
sha512(ssk, sizeof ssk, shared_secret_data);
#ifdef DEBUG_CRYPTO
ssh_print_bignum("Shared secret key", session->next_crypto->shared_secret);
ssh_log_hexdump("Shared secret key", shared_secret_data, SHA512_DIGEST_LEN);
#endif
return 0;
cleanup:
ssh_burn(ssk, sizeof ssk);
ssh_burn(hss, sizeof hss);
return rc;
}

View File

@@ -444,7 +444,8 @@ int ssh_socket_pollcallback(struct ssh_poll_handle_struct *p,
/** @internal
* @brief returns the poll handle corresponding to the socket,
* creates it if it does not exist.
* @returns allocated and initialized ssh_poll_handle object
*
* @return allocated and initialized ssh_poll_handle object
*/
ssh_poll_handle ssh_socket_get_poll_handle(ssh_socket s)
{
@@ -1435,7 +1436,7 @@ ssh_socket_connect_proxyjump(ssh_socket s)
session = s->session;
SSH_LOG(SSH_LOG_INFO,
SSH_LOG(SSH_LOG_DEBUG,
"Connecting to host %s port %d user %s through ProxyJump",
session->opts.host,
session->opts.port,
@@ -1515,7 +1516,7 @@ ssh_socket_connect_proxyjump(ssh_socket s)
/* transferred to the jump_thread_data */
jump_session = NULL;
SSH_LOG(SSH_LOG_INFO,
SSH_LOG(SSH_LOG_DEBUG,
"Starting proxy thread to host %s port %d user %s, callbacks=%p",
jump_thread_data->next_jump->hostname,
jump_thread_data->next_jump->port,

View File

@@ -38,7 +38,6 @@ static struct ssh_threads_callbacks_struct *user_callbacks = NULL;
/** @internal
* @brief inits the threading with the backend cryptographic libraries
*/
int ssh_threads_init(void)
{
static int threads_initialized = 0;

View File

@@ -259,6 +259,20 @@ else()
set(PUTTYGEN_EXECUTABLE "/bin/puttygen-not-found")
endif()
find_program(TINYSSHD_EXECUTABLE
NAMES
tinysshd
PATHS
/sbin
/usr/sbin
/usr/local/sbin)
if (TINYSSHD_EXECUTABLE)
message(STATUS "Found TinySSH server: ${TINYSSHD_EXECUTABLE}")
else()
message(STATUS "Could NOT find TinySSH server")
endif()
find_program(SSHD_EXECUTABLE
NAME
sshd
@@ -391,6 +405,14 @@ if (CLIENT_TESTING OR SERVER_TESTING)
file(COPY keys/id_ed25519_sk DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/home/bob/.ssh/ FILE_PERMISSIONS OWNER_READ OWNER_WRITE)
file(COPY keys/id_ed25519_sk.pub DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/home/bob/.ssh/ FILE_PERMISSIONS OWNER_READ OWNER_WRITE)
# TINYSSH Key Setup
set(TINYSSH_KEY_DIR "${CMAKE_CURRENT_BINARY_DIR}/etc/tinyssh")
file(MAKE_DIRECTORY ${TINYSSH_KEY_DIR})
# Copy the TINYSSH hostkeys
file(COPY keys/ed25519.pk DESTINATION ${TINYSSH_KEY_DIR})
file(COPY keys/.ed25519.sk DESTINATION ${TINYSSH_KEY_DIR})
# Allow to auth with bob's public keys on alice and doe account
configure_file(keys/id_rsa.pub ${CMAKE_CURRENT_BINARY_DIR}/home/alice/.ssh/authorized_keys @ONLY)
configure_file(keys/id_rsa.pub ${CMAKE_CURRENT_BINARY_DIR}/home/doe/.ssh/authorized_keys @ONLY)
@@ -419,6 +441,9 @@ if (CLIENT_TESTING OR SERVER_TESTING)
file(READ keys/pkcs11/id_pkcs11_ecdsa_256_openssh.pub CONTENTS)
file(APPEND ${CMAKE_CURRENT_BINARY_DIR}/home/charlie/.ssh/authorized_keys "${CONTENTS}")
# Create home directory for noneuser (for "none" authentication test)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/home/noneuser/.ssh)
file(READ keys/pkcs11/id_pkcs11_ecdsa_384_openssh.pub CONTENTS)
file(APPEND ${CMAKE_CURRENT_BINARY_DIR}/home/charlie/.ssh/authorized_keys "${CONTENTS}")

View File

@@ -36,6 +36,10 @@ if (WITH_PKCS11_URI)
torture_auth_pkcs11)
endif()
if (TINYSSHD_EXECUTABLE AND NCAT_EXECUTABLE)
set(LIBSSH_CLIENT_TESTS ${LIBSSH_CLIENT_TESTS} torture_tinyssh)
endif()
if (HAVE_PTHREAD)
set(LIBSSH_CLIENT_TESTS
${LIBSSH_CLIENT_TESTS}
@@ -92,6 +96,12 @@ foreach(_CLI_TEST ${LIBSSH_CLIENT_TESTS})
LINK_LIBRARIES ${TORTURE_LIBRARY} util
)
if (_CLI_TEST STREQUAL "torture_tinyssh")
target_compile_definitions(${_CLI_TEST} PRIVATE
TINYSSH_KEYS_DIR="${TINYSSH_KEY_DIR}"
)
endif()
if (OSX)
set_property(
TEST

View File

@@ -228,6 +228,44 @@ static void torture_auth_none_max_tries(void **state) {
torture_update_sshd_config(state, "");
}
static void torture_auth_none_success(void **state)
{
struct torture_state *s = *state;
const char *additional_config = "PermitEmptyPasswords yes\n"
"PasswordAuthentication yes\n"
"KbdInteractiveAuthentication no\n"
"PubkeyAuthentication no\n"
"AuthenticationMethods none\n";
ssh_session session = s->ssh.session;
int rc;
torture_update_sshd_config(state, additional_config);
/* Use noneuser which has an empty password set in shadow.in
* When PermitEmptyPasswords is yes and PasswordAuthentication is yes,
* OpenSSH's userauth_none() internally calls mm_auth_password() with
* an empty password, which succeeds for users with empty passwords.
*/
rc = ssh_options_set(session, SSH_OPTIONS_USER, TORTURE_SSH_USER_NONEUSER);
if (rc != SSH_OK) {
goto cleanup;
}
rc = ssh_connect(session);
if (rc != SSH_OK) {
goto cleanup;
}
rc = ssh_userauth_none(session, NULL);
assert_int_equal(rc, SSH_AUTH_SUCCESS);
cleanup:
torture_update_sshd_config(state, "");
if (rc != SSH_OK && rc != SSH_AUTH_SUCCESS) {
assert_int_equal(rc, SSH_OK);
}
}
static void torture_auth_pubkey(void **state) {
struct torture_state *s = *state;
@@ -1373,6 +1411,9 @@ int torture_run_tests(void) {
cmocka_unit_test_setup_teardown(torture_auth_none_nonblocking,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_none_success,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_none_max_tries,
session_setup,
session_teardown),
@@ -1424,9 +1465,10 @@ int torture_run_tests(void) {
cmocka_unit_test_setup_teardown(torture_auth_agent_identities_only,
agent_setup,
agent_teardown),
cmocka_unit_test_setup_teardown(torture_auth_agent_identities_only_protected,
agent_setup,
agent_teardown),
cmocka_unit_test_setup_teardown(
torture_auth_agent_identities_only_protected,
agent_setup,
agent_teardown),
cmocka_unit_test_setup_teardown(torture_auth_pubkey_types,
pubkey_setup,
session_teardown),
@@ -1436,15 +1478,17 @@ int torture_run_tests(void) {
cmocka_unit_test_setup_teardown(torture_auth_pubkey_types_ecdsa,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_pubkey_types_ecdsa_nonblocking,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(
torture_auth_pubkey_types_ecdsa_nonblocking,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_pubkey_types_ed25519,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_pubkey_types_ed25519_nonblocking,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(
torture_auth_pubkey_types_ed25519_nonblocking,
pubkey_setup,
session_teardown),
#ifdef WITH_FIDO2
cmocka_unit_test_setup_teardown(torture_auth_pubkey_types_sk_ecdsa,
pubkey_setup,
@@ -1456,9 +1500,10 @@ int torture_run_tests(void) {
cmocka_unit_test_setup_teardown(torture_auth_pubkey_rsa_key_size,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_pubkey_rsa_key_size_nonblocking,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(
torture_auth_pubkey_rsa_key_size_nonblocking,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_pubkey_skip_none,
pubkey_setup,
session_teardown),

View File

@@ -10,6 +10,7 @@
#include "libssh/callbacks.h"
#include "libssh/libssh.h"
#include <libssh/agent.h>
#include "libssh/priv.h"
#include <errno.h>
@@ -286,6 +287,96 @@ static void torture_auth_agent_forwarding(void **state)
}
}
static int agent_session_setup(void **state)
{
struct torture_state *s = *state;
int verbosity = torture_libssh_verbosity();
int rc;
s->ssh.ssh.session = ssh_new();
assert_non_null(s->ssh.ssh.session);
rc = ssh_options_set(s->ssh.ssh.session,
SSH_OPTIONS_LOG_VERBOSITY,
&verbosity);
assert_int_equal(rc, SSH_OK);
/* No callbacks needed — only talking to the local agent.
* The group setup already started the agent and loaded keys.
* Do NOT call torture_setup_ssh_agent here — that would spawn
* a second agent and overwrite SSH_AUTH_SOCK. */
s->ssh.ssh.cb_state = NULL;
s->ssh.ssh.callbacks = NULL;
return 0;
}
static void torture_agent_remove_identity(void **state)
{
struct torture_state *s = *state;
ssh_session session = s->ssh.ssh.session;
ssh_key key = NULL;
char *comment = NULL;
uint32_t count_before = 0;
uint32_t count_after = 0;
int rc;
assert_non_null(session);
assert_true(ssh_agent_is_running(session));
count_before = ssh_agent_get_ident_count(session);
assert_true(count_before > 0);
key = ssh_agent_get_first_ident(session, &comment);
assert_non_null(key);
assert_non_null(comment);
rc = ssh_agent_remove_identity(session, key);
assert_int_equal(rc, SSH_OK);
count_after = ssh_agent_get_ident_count(session);
assert_int_equal(count_after, count_before - 1);
ssh_key_free(key);
ssh_string_free_char(comment);
}
static void torture_agent_remove_identity_negative(void **state)
{
struct torture_state *s = *state;
ssh_session session = s->ssh.ssh.session;
int rc;
assert_non_null(session);
/* NULL key should return SSH_ERROR */
rc = ssh_agent_remove_identity(session, NULL);
assert_int_equal(rc, SSH_ERROR);
}
static void torture_agent_remove_identity_nonexistent(void **state)
{
struct torture_state *s = *state;
ssh_session session = s->ssh.ssh.session;
ssh_key key = NULL;
int rc;
assert_non_null(session);
assert_true(ssh_agent_is_running(session));
rc = ssh_pki_generate_key(SSH_KEYTYPE_RSA, NULL, &key);
assert_int_equal(rc, SSH_OK);
assert_non_null(key);
/* Key not in agent should fail */
rc = ssh_agent_remove_identity(session, key);
assert_int_equal(rc, SSH_ERROR);
ssh_key_free(key);
}
/* Session setup function that configures SSH agent */
static int session_setup(void **state)
{
@@ -300,7 +391,6 @@ static int session_setup(void **state)
/* Create a new session */
s->ssh.ssh.session = ssh_new();
assert_non_null(s->ssh.ssh.session);
rc = ssh_options_set(s->ssh.ssh.session,
SSH_OPTIONS_LOG_VERBOSITY,
&verbosity);
@@ -342,19 +432,32 @@ static int session_setup(void **state)
int torture_run_tests(void)
{
int rc;
struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(torture_auth_agent_forwarding,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_agent_remove_identity,
agent_session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_agent_remove_identity_negative,
agent_session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_agent_remove_identity_nonexistent,
agent_session_setup,
session_teardown),
};
ssh_init();
/* Simplify the CMocka test filter handling */
#if defined HAVE_CMOCKA_SET_TEST_FILTER
cmocka_set_message_output(CM_OUTPUT_STDOUT);
#endif
/* Apply test filtering */
torture_filter_tests(tests);
rc = cmocka_run_group_tests(tests,
@@ -365,5 +468,4 @@ int torture_run_tests(void)
return rc;
}
#endif

View File

@@ -0,0 +1,328 @@
/*
* This file is part of the SSH Library
*
* Copyright (c) 2026 by Your Bulitha Kawushika De Zoysa
*
* 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 "tests_config.h"
#define LIBSSH_STATIC
#include "libssh/libssh.h"
#include "libssh/priv.h"
#include "libssh/session.h"
#include "torture.h"
#include <errno.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define TINYSSH_PIDFILE "tinyssh.pid"
#define TINYSSH_PORT 22
/* TINYSSH Server Setup and Teardown */
static int tinyssh_setup(void **state)
{
struct torture_state *s = NULL;
char cmd[4096];
char pid_path[1024];
int rc;
torture_setup_socket_dir(state);
s = *state;
snprintf(pid_path,
sizeof(pid_path),
"%s/%s",
s->socket_dir,
TINYSSH_PIDFILE);
free(s->srv_pidfile);
s->srv_pidfile = strdup(pid_path);
if (s->srv_pidfile == NULL) {
return -1;
}
snprintf(cmd,
sizeof(cmd),
"%s -l %s %d -k -c \"%s %s -v %s\" "
"> %s/tinyssh.log 2>&1 & echo $! > %s",
NCAT_EXECUTABLE,
TORTURE_SSH_SERVER,
TINYSSH_PORT,
TINYSSHD_EXECUTABLE,
"",
TINYSSH_KEYS_DIR,
s->socket_dir,
s->srv_pidfile);
SSH_LOG(SSH_LOG_DEBUG, "Executing: %s\n", cmd);
rc = system(cmd);
if (rc != 0) {
return -1;
}
rc = torture_wait_for_daemon(15);
if (rc != 0) {
return -1;
}
return 0;
}
static int tinyssh_teardown(void **state)
{
struct torture_state *s = *state;
torture_terminate_process(s->srv_pidfile);
torture_teardown_socket_dir(state);
return 0;
}
/* LIBSSH Client Setup and Teardown */
static int session_setup(void **state)
{
struct torture_state *s = *state;
int verbosity = torture_libssh_verbosity();
bool process_config = false;
int port = TINYSSH_PORT;
struct passwd *pwd = NULL;
int rc;
pwd = getpwnam("bob");
assert_non_null(pwd);
rc = setuid(pwd->pw_uid);
assert_return_code(rc, errno);
s->ssh.session = ssh_new();
assert_non_null(s->ssh.session);
ssh_options_set(s->ssh.session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
ssh_options_set(s->ssh.session, SSH_OPTIONS_HOST, TORTURE_SSH_SERVER);
ssh_options_set(s->ssh.session, SSH_OPTIONS_PORT, &port);
ssh_options_set(s->ssh.session, SSH_OPTIONS_USER, "bob");
ssh_options_set(s->ssh.session,
SSH_OPTIONS_PROCESS_CONFIG,
&process_config);
return 0;
}
static int session_teardown(void **state)
{
struct torture_state *s = *state;
if (s->ssh.session) {
ssh_disconnect(s->ssh.session);
ssh_free(s->ssh.session);
}
return 0;
}
/* Algorithms Helper */
static void test_specific_algorithm(ssh_session session,
const char *kex,
const char *cipher,
const char *hostkey,
int expected_rc)
{
int rc;
char data[256];
size_t len_to_test[] = {1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 15,
16, 20, 31, 32, 33, 63, 64, 65, 100, 127, 128};
unsigned int i;
/* Set Key Exchange */
if (kex != NULL) {
rc = ssh_options_set(session, SSH_OPTIONS_KEY_EXCHANGE, kex);
assert_ssh_return_code(session, rc);
}
/* Set Ciphers */
if (cipher != NULL) {
rc = ssh_options_set(session, SSH_OPTIONS_CIPHERS_C_S, cipher);
assert_ssh_return_code(session, rc);
rc = ssh_options_set(session, SSH_OPTIONS_CIPHERS_S_C, cipher);
assert_ssh_return_code(session, rc);
}
/* Set Hostkey */
if (hostkey != NULL) {
rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, hostkey);
assert_ssh_return_code(session, rc);
}
rc = ssh_connect(session);
if (expected_rc == SSH_OK) {
assert_ssh_return_code(session, rc);
if (cipher != NULL) {
const char *used_cipher = ssh_get_cipher_out(session);
assert_non_null(used_cipher);
assert_string_equal(used_cipher, cipher);
}
if (hostkey != NULL) {
ssh_key pubkey = NULL;
const char *type_str = NULL;
rc = ssh_get_server_publickey(session, &pubkey);
assert_int_equal(rc, SSH_OK);
assert_non_null(pubkey);
type_str = ssh_key_type_to_char(ssh_key_type(pubkey));
assert_non_null(type_str);
assert_string_equal(type_str, hostkey);
ssh_key_free(pubkey);
}
memset(data, 0, sizeof(data));
for (i = 0; i < (sizeof(len_to_test) / sizeof(size_t)); i++) {
memset(data, 'A', len_to_test[i]);
ssh_send_ignore(session, data);
ssh_handle_packets(session, 50);
}
rc = ssh_userauth_none(session, NULL);
if (rc != SSH_OK) {
rc = ssh_get_error_code(session);
assert_int_equal(rc, SSH_REQUEST_DENIED);
}
} else {
assert_int_not_equal(rc, SSH_OK);
}
}
/* Test Cases */
static void torture_tinyssh_curve25519(void **state)
{
struct torture_state *s = *state;
test_specific_algorithm(s->ssh.session,
"curve25519-sha256",
NULL,
NULL,
SSH_OK);
}
static void torture_tinyssh_curve25519_libssh(void **state)
{
struct torture_state *s = *state;
test_specific_algorithm(s->ssh.session,
"curve25519-sha256@libssh.org",
NULL,
NULL,
SSH_OK);
}
static void torture_tinyssh_sntrup761(void **state)
{
struct torture_state *s = *state;
test_specific_algorithm(s->ssh.session,
"sntrup761x25519-sha512@openssh.com",
NULL,
NULL,
SSH_OK);
}
static void torture_tinyssh_chacha20(void **state)
{
struct torture_state *s = *state;
test_specific_algorithm(s->ssh.session,
NULL,
"chacha20-poly1305@openssh.com",
NULL,
SSH_OK);
}
static void torture_tinyssh_neg_cipher(void **state)
{
struct torture_state *s = *state;
/* TinySSH does not support older ciphers like aes128-cbc.*/
test_specific_algorithm(s->ssh.session,
NULL,
"aes128-cbc",
NULL,
SSH_ERROR);
}
static void torture_tinyssh_hostkey_ed25519(void **state)
{
struct torture_state *s = *state;
test_specific_algorithm(s->ssh.session, NULL, NULL, "ssh-ed25519", SSH_OK);
}
static void torture_tinyssh_neg_kex(void **state)
{
struct torture_state *s = *state;
/* TinySSH does not support legacy Diffie-Hellman groups or NIST curves.*/
test_specific_algorithm(s->ssh.session,
"diffie-hellman-group1-sha1",
NULL,
NULL,
SSH_ERROR);
}
int torture_run_tests(void)
{
int rc;
struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(torture_tinyssh_curve25519,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_tinyssh_curve25519_libssh,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_tinyssh_sntrup761,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_tinyssh_hostkey_ed25519,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_tinyssh_chacha20,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_tinyssh_neg_cipher,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_tinyssh_neg_kex,
session_setup,
session_teardown),
};
ssh_init();
torture_filter_tests(tests);
rc = cmocka_run_group_tests(tests, tinyssh_setup, tinyssh_teardown);
ssh_finalize();
return rc;
}

View File

@@ -2,3 +2,4 @@ bob:secret:sshd
alice:secret:sshd
charlie:secret:sshd
doe:secret:sshd
noneuser::sshd

View File

@@ -3,6 +3,7 @@ alice:x:5001:9000:alice gecos:@HOMEDIR@/alice:/bin/sh
charlie:x:5002:9000:charlie gecos:@HOMEDIR@/charlie:/bin/sh
doe:x:5003:9000:doe gecos:@HOMEDIR@/doe:/bin/sh
frank:x:5003:9000:doe gecos:@HOMEDIR@/frank:/bin/sh
noneuser:x:5004:9000:noneuser gecos:@HOMEDIR@/noneuser:/bin/sh
sshd:x:65530:65531:sshd:@HOMEDIR@:/sbin/nologin
nobody:x:65533:65534:nobody gecos:@HOMEDIR@:/bin/false
root:x:0:0:root gecos:@HOMEDIR@:/bin/false

View File

@@ -2,3 +2,4 @@ alice:$6$0jWkA8VP$MvBUvtGy38jWCZ5KtqnZEKQWXvvImDkDhDQII1kTqtAp3/xH31b71c.AjGkBFl
bob:$6$0jWkA8VP$MvBUvtGy38jWCZ5KtqnZEKQWXvvImDkDhDQII1kTqtAp3/xH31b71c.AjGkBFle.2QwCJQH7OzB/NXiMprusr/::0:::::
charlie:$6$0jWkA8VP$MvBUvtGy38jWCZ5KtqnZEKQWXvvImDkDhDQII1kTqtAp3/xH31b71c.AjGkBFle.2QwCJQH7OzB/NXiMprusr/::0:::::
doe:$6$0jWkA8VP$MvBUvtGy38jWCZ5KtqnZEKQWXvvImDkDhDQII1kTqtAp3/xH31b71c.AjGkBFle.2QwCJQH7OzB/NXiMprusr/::0:::::
noneuser:::0:::::

View File

@@ -1,8 +1,19 @@
project(fuzzing CXX)
# Build SSH server mock helper as object library
add_library(ssh_server_mock_obj OBJECT ssh_server_mock.c)
target_link_libraries(ssh_server_mock_obj PRIVATE ${TORTURE_LINK_LIBRARIES})
macro(fuzzer name)
add_executable(${name} ${name}.c)
target_link_libraries(${name} PRIVATE ${TORTURE_LINK_LIBRARIES})
# Add ssh_server_mock dependency for scp and sftp fuzzers
if (${name} STREQUAL "ssh_scp_fuzzer" OR ${name} STREQUAL "ssh_sftp_fuzzer")
target_sources(${name} PRIVATE $<TARGET_OBJECTS:ssh_server_mock_obj>)
target_link_libraries(${name} PRIVATE pthread)
endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set_target_properties(${name}
PROPERTIES
@@ -33,8 +44,10 @@ fuzzer(ssh_client_config_fuzzer)
fuzzer(ssh_known_hosts_fuzzer)
fuzzer(ssh_privkey_fuzzer)
fuzzer(ssh_pubkey_fuzzer)
fuzzer(ssh_sftp_attr_fuzzer)
fuzzer(ssh_sshsig_fuzzer)
if (WITH_SERVER)
fuzzer(ssh_scp_fuzzer)
fuzzer(ssh_server_fuzzer)
fuzzer(ssh_bind_config_fuzzer)
endif (WITH_SERVER)

206
tests/fuzz/ssh_scp_fuzzer.c Normal file
View File

@@ -0,0 +1,206 @@
/*
* Copyright 2026 libssh authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <assert.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define LIBSSH_STATIC 1
#include <libssh/libssh.h>
#include <libssh/scp.h>
#include "nallocinc.c"
#include "ssh_server_mock.h"
static void _fuzz_finalize(void)
{
ssh_finalize();
}
int LLVMFuzzerInitialize(int *argc, char ***argv)
{
(void)argc;
nalloc_init(*argv[0]);
ssh_init();
atexit(_fuzz_finalize);
ssh_mock_write_hostkey(SSH_MOCK_HOSTKEY_PATH);
return 0;
}
/* Helper function to test one cipher/HMAC combination */
static int test_scp_with_cipher(const uint8_t *data,
size_t size,
const char *cipher,
const char *hmac)
{
int socket_fds[2] = {-1, -1};
ssh_session client_session = NULL;
ssh_scp scp = NULL, scp_recursive = NULL;
char buf[256] = {0};
pthread_t srv_thread;
/* Configure mock SSH server with fuzzer data */
struct ssh_mock_server_config server_config = {
.protocol_data = data,
.protocol_data_size = size,
.exec_callback = ssh_mock_send_raw_data,
.subsystem_callback = NULL,
.callback_userdata = NULL,
.cipher = cipher,
.hmac = hmac,
.server_socket = -1,
.client_socket = -1,
.server_ready = false,
.server_error = false,
};
if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fds) != 0) {
goto cleanup;
}
server_config.server_socket = socket_fds[0];
server_config.client_socket = socket_fds[1];
if (ssh_mock_server_start(&server_config, &srv_thread) != 0) {
goto cleanup;
}
client_session = ssh_new();
if (client_session == NULL) {
goto cleanup_thread;
}
/* Configure client with specified cipher/HMAC */
ssh_options_set(client_session, SSH_OPTIONS_FD, &socket_fds[1]);
ssh_options_set(client_session, SSH_OPTIONS_HOST, "localhost");
ssh_options_set(client_session, SSH_OPTIONS_USER, "fuzz");
ssh_options_set(client_session, SSH_OPTIONS_CIPHERS_C_S, cipher);
ssh_options_set(client_session, SSH_OPTIONS_CIPHERS_S_C, cipher);
ssh_options_set(client_session, SSH_OPTIONS_HMAC_C_S, hmac);
ssh_options_set(client_session, SSH_OPTIONS_HMAC_S_C, hmac);
/* Set timeout for operations (1 second) */
long timeout = 1;
ssh_options_set(client_session, SSH_OPTIONS_TIMEOUT, &timeout);
if (ssh_connect(client_session) != SSH_OK) {
goto cleanup_thread;
}
if (ssh_userauth_none(client_session, NULL) != SSH_AUTH_SUCCESS) {
goto cleanup_thread;
}
scp = ssh_scp_new(client_session, SSH_SCP_READ, "/tmp/fuzz");
if (scp == NULL) {
goto cleanup_thread;
}
if (ssh_scp_init(scp) != SSH_OK) {
goto cleanup_thread;
}
if (size > 0) {
size_t copy_size = size < sizeof(buf) ? size : sizeof(buf);
memcpy(buf, data, copy_size);
}
/* Fuzz all SCP API functions in read mode */
ssh_scp_pull_request(scp);
ssh_scp_request_get_filename(scp);
ssh_scp_request_get_permissions(scp);
ssh_scp_request_get_size64(scp);
ssh_scp_request_get_size(scp);
ssh_scp_request_get_warning(scp);
ssh_scp_accept_request(scp);
ssh_scp_deny_request(scp, "Denied by fuzzer");
ssh_scp_read(scp, buf, sizeof(buf));
/* Final fuzz of scp pull request after all the calls */
ssh_scp_pull_request(scp);
/* Fuzz SCP in write/upload + recursive directory mode. */
scp_recursive = ssh_scp_new(client_session,
SSH_SCP_WRITE | SSH_SCP_RECURSIVE,
"/tmp/fuzz-recursive");
if (scp_recursive != NULL) {
if (ssh_scp_init(scp_recursive) == SSH_OK) {
ssh_scp_push_directory(scp_recursive, "fuzz-dir", 0755);
ssh_scp_push_file(scp_recursive, "fuzz-file", sizeof(buf), 0644);
ssh_scp_write(scp_recursive, buf, sizeof(buf));
ssh_scp_leave_directory(scp_recursive);
}
}
cleanup_thread:
pthread_join(srv_thread, NULL);
cleanup:
if (scp_recursive != NULL) {
ssh_scp_close(scp_recursive);
ssh_scp_free(scp_recursive);
}
if (scp) {
ssh_scp_close(scp);
ssh_scp_free(scp);
}
if (client_session) {
ssh_disconnect(client_session);
ssh_free(client_session);
}
if (socket_fds[0] >= 0)
close(socket_fds[0]);
if (socket_fds[1] >= 0)
close(socket_fds[1]);
return 0;
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
assert(nalloc_start(data, size) > 0);
/* Test all cipher/HMAC combinations exhaustively */
const char *ciphers[] = {
"none",
"aes128-ctr",
"aes256-ctr",
"aes128-cbc",
};
const char *hmacs[] = {
"none",
"hmac-sha1",
"hmac-sha2-256",
};
int num_ciphers = sizeof(ciphers) / sizeof(ciphers[0]);
int num_hmacs = sizeof(hmacs) / sizeof(hmacs[0]);
for (int i = 0; i < num_ciphers; i++) {
for (int j = 0; j < num_hmacs; j++) {
test_scp_with_cipher(data, size, ciphers[i], hmacs[j]);
}
}
nalloc_end();
return 0;
}

View File

@@ -0,0 +1 @@
C0644 50 ../../../etc/passwd

View File

@@ -0,0 +1 @@
C0644 10 dir/file.txt

View File

@@ -0,0 +1 @@
C 100 test

View File

@@ -0,0 +1 @@
C0644 10 ..

View File

@@ -0,0 +1 @@
C0755 1024 executable.sh

View File

@@ -0,0 +1 @@
C0644 999999999999 huge.dat

View File

@@ -0,0 +1 @@
T1234567890 0 1234567890 0

View File

@@ -0,0 +1 @@
C0644 100 test.txt

View File

@@ -0,0 +1 @@
Warning: Test warning

View File

@@ -0,0 +1 @@
C0644 10 .

View File

@@ -0,0 +1 @@
Error: Test error

View File

@@ -0,0 +1 @@
Xunknown command

View File

@@ -0,0 +1 @@
C0644 test

View File

@@ -0,0 +1 @@
D0755 0 mydir

View File

@@ -0,0 +1 @@
C0644 abc test

View File

@@ -0,0 +1,262 @@
/*
* Copyright 2026 libssh authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ssh_server_mock.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define LIBSSH_STATIC 1
#include <libssh/callbacks.h>
#include <libssh/libssh.h>
#include <libssh/server.h>
/* Fixed ed25519 key for all mock servers */
const char *ssh_mock_ed25519_key_pem =
"-----BEGIN OPENSSH PRIVATE KEY-----\n"
"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n"
"QyNTUxOQAAACBpFO8/JfYlIqg6+vqx1vDKWDqxJHxw4tBqnQfiOjf2zAAAAJgbsYq1G7GK\n"
"tQAAAAtzc2gtZWQyNTUxOQAAACBpFO8/JfYlIqg6+vqx1vDKWDqxJHxw4tBqnQfiOjf2zA\n"
"AAAEAkGaLvQwKMbGVRk2M8cz7gqWvpBKuHkuekJxIBQrUJl2kU7z8l9iUiqDr6+rHW8MpY\n"
"OrEkfHDi0GqdB+I6N/bMAAAAEGZ1enotZWQyNTUxOS1rZXkBAgMEBQ==\n"
"-----END OPENSSH PRIVATE KEY-----\n";
/* Internal server session data */
struct mock_session_data {
ssh_channel channel;
struct ssh_mock_server_config *config;
};
/* Auth callback - always accepts "none" auth */
static int mock_auth_none(ssh_session session, const char *user, void *userdata)
{
(void)session;
(void)user;
(void)userdata;
return SSH_AUTH_SUCCESS;
}
/* Channel open callback */
static ssh_channel mock_channel_open(ssh_session session, void *userdata)
{
struct mock_session_data *sdata = (struct mock_session_data *)userdata;
sdata->channel = ssh_channel_new(session);
return sdata->channel;
}
/* Exec request callback - for SCP */
static int mock_channel_exec(ssh_session session,
ssh_channel channel,
const char *command,
void *userdata)
{
struct mock_session_data *sdata = (struct mock_session_data *)userdata;
(void)session;
(void)command;
if (sdata->config->exec_callback) {
return sdata->config->exec_callback(channel,
sdata->config->protocol_data,
sdata->config->protocol_data_size,
sdata->config->callback_userdata);
}
return SSH_OK;
}
/* Subsystem request callback - for SFTP */
static int mock_channel_subsystem(ssh_session session,
ssh_channel channel,
const char *subsystem,
void *userdata)
{
struct mock_session_data *sdata = (struct mock_session_data *)userdata;
(void)session;
(void)subsystem;
if (sdata->config->subsystem_callback) {
return sdata->config->subsystem_callback(
channel,
sdata->config->protocol_data,
sdata->config->protocol_data_size,
sdata->config->callback_userdata);
}
return SSH_OK;
}
/* Server thread implementation */
static void *server_thread_func(void *arg)
{
struct ssh_mock_server_config *config =
(struct ssh_mock_server_config *)arg;
ssh_bind sshbind = NULL;
ssh_session session = NULL;
ssh_event event = NULL;
struct mock_session_data sdata = {0};
sdata.config = config;
struct ssh_server_callbacks_struct server_cb = {
.userdata = &sdata,
.auth_none_function = mock_auth_none,
.channel_open_request_session_function = mock_channel_open,
};
struct ssh_channel_callbacks_struct channel_cb = {
.userdata = &sdata,
.channel_exec_request_function = mock_channel_exec,
.channel_subsystem_request_function = mock_channel_subsystem,
};
bool no = false;
sshbind = ssh_bind_new();
if (sshbind == NULL) {
config->server_error = true;
return NULL;
}
session = ssh_new();
if (session == NULL) {
ssh_bind_free(sshbind);
config->server_error = true;
return NULL;
}
const char *cipher = config->cipher ? config->cipher : "aes128-ctr";
const char *hmac = config->hmac ? config->hmac : "hmac-sha1";
ssh_bind_options_set(sshbind,
SSH_BIND_OPTIONS_HOSTKEY,
SSH_MOCK_HOSTKEY_PATH);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_CIPHERS_C_S, cipher);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_CIPHERS_S_C, cipher);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HMAC_C_S, hmac);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HMAC_S_C, hmac);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_PROCESS_CONFIG, &no);
ssh_set_auth_methods(session, SSH_AUTH_METHOD_NONE);
ssh_callbacks_init(&server_cb);
ssh_set_server_callbacks(session, &server_cb);
if (ssh_bind_accept_fd(sshbind, session, config->server_socket) != SSH_OK) {
ssh_free(session);
ssh_bind_free(sshbind);
config->server_error = true;
return NULL;
}
config->server_ready = true;
event = ssh_event_new();
if (event == NULL) {
ssh_disconnect(session);
ssh_free(session);
ssh_bind_free(sshbind);
return NULL;
}
if (ssh_handle_key_exchange(session) == SSH_OK) {
ssh_event_add_session(event, session);
for (int i = 0; i < 50 && !sdata.channel; i++) {
ssh_event_dopoll(event, 1);
}
if (sdata.channel) {
ssh_callbacks_init(&channel_cb);
ssh_set_channel_callbacks(sdata.channel, &channel_cb);
int max_iterations = 30;
for (int iter = 0; iter < max_iterations &&
!ssh_channel_is_closed(sdata.channel) &&
!ssh_channel_is_eof(sdata.channel);
iter++) {
if (ssh_event_dopoll(event, 100) == SSH_ERROR) {
break;
}
}
}
}
if (event)
ssh_event_free(event);
if (session) {
ssh_disconnect(session);
ssh_free(session);
}
if (sshbind)
ssh_bind_free(sshbind);
return NULL;
}
/* Public API - start mock SSH server */
int ssh_mock_server_start(struct ssh_mock_server_config *config,
pthread_t *thread)
{
if (!config || !thread)
return -1;
config->server_ready = false;
config->server_error = false;
if (pthread_create(thread, NULL, server_thread_func, config) != 0) {
return -1;
}
for (int i = 0; i < 50 && !config->server_ready && !config->server_error;
i++) {
usleep(100);
}
return config->server_error ? -1 : 0;
}
/* Generic protocol callback - sends raw fuzzer data for any protocol */
int ssh_mock_send_raw_data(void *channel,
const void *data,
size_t size,
void *userdata)
{
(void)userdata;
ssh_channel target_channel = (ssh_channel)channel;
/* Send raw fuzzer data - let protocol parser interpret it */
if (size > 0) {
ssh_channel_write(target_channel, data, size);
}
/* Close channel to signal completion */
ssh_channel_send_eof(target_channel);
ssh_channel_close(target_channel);
return SSH_OK;
}
/* Write fixed ed25519 host key to file */
int ssh_mock_write_hostkey(const char *path)
{
FILE *fp = fopen(path, "wb");
if (fp == NULL)
return -1;
size_t len = strlen(ssh_mock_ed25519_key_pem);
size_t nwritten = fwrite(ssh_mock_ed25519_key_pem, 1, len, fp);
fclose(fp);
return (nwritten == len) ? 0 : -1;
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2026 libssh authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef SSH_SERVER_MOCK_H
#define SSH_SERVER_MOCK_H
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
/* Server callback type */
typedef int (*ssh_mock_callback_fn)(void *channel,
const void *data,
size_t size,
void *userdata);
/* Mock server configuration */
struct ssh_mock_server_config {
const uint8_t *protocol_data;
size_t protocol_data_size;
ssh_mock_callback_fn exec_callback;
ssh_mock_callback_fn subsystem_callback;
void *callback_userdata;
const char *cipher;
const char *hmac;
int server_socket;
int client_socket;
bool server_ready;
bool server_error;
};
/* Public API functions */
int ssh_mock_server_start(struct ssh_mock_server_config *config,
pthread_t *thread);
int ssh_mock_send_raw_data(void *channel,
const void *data,
size_t size,
void *userdata);
int ssh_mock_write_hostkey(const char *path);
/* Fixed ed25519 key constant */
extern const char *ssh_mock_ed25519_key_pem;
/* Centralized hostkey path used by all mock servers */
#define SSH_MOCK_HOSTKEY_PATH "/tmp/libssh_mock_fuzz_key"
#endif

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2026 libssh authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LIBSSH_STATIC 1
#include "libssh/libssh.h"
#include "libssh/sftp.h"
#include "libssh/sftp_priv.h"
#include "nallocinc.c"
/* SFTP protocol version constants */
#define SFTP_PROTOCOL_VERSION_3 3
#define SFTP_PROTOCOL_VERSION_4 4
/* Flags for sftp_parse_attr expectname parameter */
#define SFTP_EXPECT_NAME 1
#define SFTP_NO_NAME 0
/*
* Helper to create a minimal sftp_session for fuzzing.
* We don't use sftp_new() as it requires a real SSH connection.
*/
static sftp_session create_minimal_sftp_session(ssh_session session)
{
sftp_session sftp;
sftp = calloc(1, sizeof(struct sftp_session_struct));
if (sftp == NULL) {
return NULL;
}
sftp->session = session;
return sftp;
}
static void _fuzz_finalize(void)
{
ssh_finalize();
}
int LLVMFuzzerInitialize(int *argc, char ***argv)
{
(void)argc;
nalloc_init(*argv[0]);
ssh_init();
atexit(_fuzz_finalize);
return 0;
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
ssh_session session = NULL;
sftp_session sftp = NULL;
ssh_buffer buffer = NULL;
sftp_attributes attr = NULL;
int versions[] = {
SFTP_PROTOCOL_VERSION_3, SFTP_PROTOCOL_VERSION_3,
SFTP_PROTOCOL_VERSION_4, SFTP_PROTOCOL_VERSION_4
};
int expectnames[] = {SFTP_NO_NAME, SFTP_EXPECT_NAME, SFTP_NO_NAME, SFTP_EXPECT_NAME};
size_t i;
/* Minimum bytes for a valid SFTP message */
if (size == 0) {
return 0;
}
assert(nalloc_start(data, size) > 0);
/* Allocate shared resources once for all test iterations */
session = ssh_new();
if (session == NULL) {
goto cleanup;
}
sftp = create_minimal_sftp_session(session);
if (sftp == NULL) {
goto cleanup;
}
buffer = ssh_buffer_new();
if (buffer == NULL) {
goto cleanup;
}
/* Main fuzzing target: sftp_parse_attr */
/* Parses untrusted SFTP messages from client */
/* Test all combinations (v3/v4, with/without name) */
for (i = 0; i < (sizeof(versions) / sizeof(versions[0])); i++) {
sftp->version = versions[i];
/* Reset and repopulate buffer for each iteration */
ssh_buffer_reinit(buffer);
if (ssh_buffer_add_data(buffer, data, size) == SSH_OK) {
attr = sftp_parse_attr(sftp, buffer, expectnames[i]);
sftp_attributes_free(attr);
attr = NULL;
}
}
cleanup:
ssh_buffer_free(buffer);
free(sftp);
ssh_free(session);
nalloc_end();
return 0;
}

BIN
tests/keys/.ed25519.sk Normal file

Binary file not shown.

1
tests/keys/ed25519.pk Normal file
View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>C<EFBFBD><EFBFBD>K<EFBFBD>ݛ<EFBFBD>1<1C><>'j &<26>e<EFBFBD><65><EFBFBD><EF8A8D>

View File

@@ -1130,6 +1130,46 @@ static void torture_server_sftp_handles_exhaustion(void **state)
}
}
static void torture_server_sftp_opendir_handles_exhaustion(void **state)
{
struct test_server_st *tss = *state;
struct torture_state *s = NULL;
struct torture_sftp *tsftp = NULL;
char name[128] = {0};
sftp_file handles[SFTP_HANDLES] = {0};
sftp_dir dir = NULL;
sftp_session sftp = NULL;
int rc;
assert_non_null(tss);
s = tss->state;
assert_non_null(s);
tsftp = s->ssh.tsftp;
assert_non_null(tsftp);
sftp = tsftp->sftp;
assert_non_null(sftp);
/* Occupy all handles with files */
for (int i = 0; i < SFTP_HANDLES; i++) {
snprintf(name, sizeof(name), "%s/fn%d", tsftp->testdir, i);
handles[i] = sftp_open(sftp, name, O_WRONLY | O_CREAT, 0700);
assert_non_null(handles[i]);
}
/* Opening a directory should fail gracefully without leaking h->name */
dir = sftp_opendir(sftp, tsftp->testdir);
assert_null(dir);
/* cleanup */
for (int i = 0; i < SFTP_HANDLES; i++) {
rc = sftp_close(handles[i]);
assert_int_equal(rc, SSH_OK);
}
}
static void torture_server_sftp_handle_overrun(void **state)
{
struct test_server_st *tss = *state;
@@ -1290,6 +1330,9 @@ int torture_run_tests(void) {
cmocka_unit_test_setup_teardown(torture_server_sftp_handles_exhaustion,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(torture_server_sftp_opendir_handles_exhaustion,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(torture_server_sftp_handle_overrun,
session_setup_sftp,
session_teardown),

View File

@@ -86,3 +86,7 @@
#cmakedefine PKCS11SPY "${PKCS11SPY}"
#cmakedefine HAVE_SK_DUMMY 1
#cmakedefine SK_DUMMY_LIBRARY_PATH "${SK_DUMMY_LIBRARY_PATH}"
/* TinySSH Executable */
#cmakedefine TINYSSHD_EXECUTABLE "${TINYSSHD_EXECUTABLE}"

View File

@@ -51,6 +51,7 @@
#define TORTURE_SSH_USER_ALICE "alice"
#define TORTURE_SSH_USER_CHARLIE "charlie"
#define TORTURE_SSH_USER_NONEUSER "noneuser"
/* Used by main to communicate with parse_opt. */
struct argument_s {

View File

@@ -267,10 +267,10 @@ static void torture_ssh_buffer_bignum(void **state)
bignum num = NULL;
int rc;
size_t len;
uint8_t verif[] = "\x00\x00\x00\x04" /* len 4 byte */
"\x00\x00\x00\xff" /* padded 255 */
"\x00\x00\x00\x02" /* len 2 byte */
"\x00\xff"; /* padded 255 */
uint8_t verif[] = "\x00\x00\x00\x02" /* len */
"\x00\xff" /* pad, num */
"\x00\x00\x00\x02" /* len */
"\x00\xff"; /* pad, num */
(void)state;
@@ -283,20 +283,13 @@ static void torture_ssh_buffer_bignum(void **state)
rc = bignum_set_word(num, 255);
assert_int_equal(rc, 1);
rc = ssh_buffer_pack(buffer, "FB", num, (size_t)4, num);
rc = ssh_buffer_pack(buffer, "BB", num, num);
assert_int_equal(rc, SSH_OK);
len = ssh_buffer_get_len(buffer);
assert_int_equal(len, sizeof(verif) - 1);
assert_memory_equal(ssh_buffer_get(buffer), verif, sizeof(verif) - 1);
/* negative test -- this number requires 3 bytes */
rc = bignum_set_word(num, 256 * 256);
assert_int_equal(rc, 1);
rc = ssh_buffer_pack(buffer, "FB", num, (size_t)2, num);
assert_int_equal(rc, SSH_ERROR);
bignum_safe_free(num);
SSH_BUFFER_FREE(buffer);

View File

@@ -130,23 +130,23 @@ extern LIBSSH_THREAD int ssh_log_level;
"ProxyJump = many-spaces.com\n" /* valid */
/* Match keyword */
#define LIBSSH_TESTCONFIG_STRING10 \
"Match host example\n" \
"\tHostName example.com\n" \
"Match host example1,example2\n" \
"\tHostName exampleN\n" \
"Match user guest\n" \
"\tHostName guest.com\n" \
"Match user tester host testhost\n" \
"\tHostName testhost.com\n" \
#define LIBSSH_TESTCONFIG_STRING10 \
"Match host example\n" \
"\tHostName example.com\n" \
"Match host example1,example2\n" \
"\tHostName exampleN\n" \
"Match user guest\n" \
"\tHostName guest.com\n" \
"Match user tester host testhost\n" \
"\tHostName testhost.com\n" \
"Match !user tester host testhost\n" \
"\tHostName nonuser-testhost.com\n" \
"Match all\n" \
"\tHostName all-matched.com\n" \
/* Unsupported options */ \
"Match originalhost example\n" \
"\tHostName original-example.com\n" \
"Match localuser guest\n" \
"\tHostName nonuser-testhost.com\n" \
"Match all\n" \
"\tHostName all-matched.com\n" \
"Match originalhost example\n" \
"\tHostName original-example.com\n" \
"\tUser originaluser\n" \
"Match localuser guest\n" \
"\tHostName local-guest.com\n"
/* ProxyJump */
@@ -851,26 +851,40 @@ static void torture_config_match(void **state,
ssh_options_set(session, SSH_OPTIONS_HOST, "unmatched");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "all-matched.com");
assert_string_equal(session->opts.originalhost, "unmatched");
/* Hostname example does simple hostname matching */
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "example");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "example.com");
assert_string_equal(session->opts.originalhost, "example");
/* We can match also both hosts from a comma separated list */
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "example1");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "exampleN");
assert_string_equal(session->opts.originalhost, "example1");
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "example2");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "exampleN");
assert_string_equal(session->opts.originalhost, "example2");
/* We can match by user */
/* We can match by originalhost */
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "example");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "example.com");
assert_string_equal(session->opts.originalhost, "example");
/* Match originalhost sets User */
assert_string_equal(session->opts.username, "originaluser");
/* We can match by user - clear originalhost to isolate user match */
torture_reset_config(session);
SAFE_FREE(session->opts.originalhost);
ssh_options_set(session, SSH_OPTIONS_USER, "guest");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "guest.com");
@@ -881,6 +895,7 @@ static void torture_config_match(void **state,
ssh_options_set(session, SSH_OPTIONS_HOST, "testhost");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "testhost.com");
assert_string_equal(session->opts.originalhost, "testhost");
/* We can also negate conditions */
torture_reset_config(session);
@@ -888,9 +903,43 @@ static void torture_config_match(void **state,
ssh_options_set(session, SSH_OPTIONS_HOST, "testhost");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "nonuser-testhost.com");
assert_string_equal(session->opts.originalhost, "testhost");
/* In this part, we try various other config files and strings. */
/* Match host compares against resolved hostname */
config = "Host ssh-host\n"
"\tHostname 10.1.1.1\n"
"Match host 10.1.1.*\n"
"\tPort 2222\n";
if (file != NULL) {
torture_write_file(file, config);
} else {
string = config;
}
torture_reset_config(session);
session->opts.port = 0;
ssh_options_set(session, SSH_OPTIONS_HOST, "ssh-host");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "10.1.1.1");
assert_string_equal(session->opts.originalhost, "ssh-host");
assert_int_equal(session->opts.port, 2222);
/* Match host falls back to originalhost when host is NULL */
config = "Match host my_alias\n"
"\tHostName alias-matched.com\n";
if (file != NULL) {
torture_write_file(file, config);
} else {
string = config;
}
torture_reset_config(session);
SAFE_FREE(session->opts.username);
ssh_options_set(session, SSH_OPTIONS_HOST, "my_alias");
assert_null(session->opts.host);
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "alias-matched.com");
/* Match final is not completely supported, but should do quite much the
* same as "match all". The trailing "all" is not mandatory. */
config = "Match final all\n"
@@ -1018,7 +1067,7 @@ static void torture_config_match(void **state,
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "unmatched");
/* Missing argument to unsupported option originalhost */
/* Missing argument to option originalhost */
config = "Match originalhost\n"
"\tHost originalhost.com\n";
if (file != NULL) {
@@ -1289,7 +1338,6 @@ static void torture_config_proxyjump(void **state,
assert_string_equal(session->opts.ProxyCommand,
"ssh -W '[%h]:%p' 2620:52:0::fed");
/* Multiple @ is allowed in second jump */
config = "Host allowed-hostname\n"
"\tProxyJump localhost,user@principal.com@jumpbox:22\n";
@@ -1351,7 +1399,73 @@ static void torture_config_proxyjump(void **state,
"jumpbox",
"user@principal.com",
"22");
/* Non-RFC-1035 alias (underscore) — accepted with non-strict parse */
config = "Host alias-jump\n"
"\tProxyJump my_alias\n";
if (file != NULL) {
torture_write_file(file, config);
} else {
string = config;
}
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "alias-jump");
_parse_config(session, file, string, SSH_OK);
helper_proxy_jump_check(session->opts.proxy_jumps->root,
"my_alias",
NULL,
NULL);
/* Non-RFC-1035 alias in multi-hop second jump */
config = "Host alias-multi\n"
"\tProxyJump localhost,my_alias:2222\n";
if (file != NULL) {
torture_write_file(file, config);
} else {
string = config;
}
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "alias-multi");
_parse_config(session, file, string, SSH_OK);
helper_proxy_jump_check(session->opts.proxy_jumps->root,
"my_alias",
NULL,
"2222");
helper_proxy_jump_check(session->opts.proxy_jumps->root->next,
"localhost",
NULL,
NULL);
/* Non-RFC-1035 alias — proxycommand based */
torture_setenv("OPENSSH_PROXYJUMP", "1");
config = "Host alias-jump\n"
"\tProxyJump my_alias\n";
if (file != NULL) {
torture_write_file(file, config);
} else {
string = config;
}
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "alias-jump");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.ProxyCommand,
"ssh -W '[%h]:%p' my_alias");
/* Non-RFC-1035 alias in multi-hop — proxycommand based */
config = "Host alias-multi\n"
"\tProxyJump localhost,my_alias:2222\n";
if (file != NULL) {
torture_write_file(file, config);
} else {
string = config;
}
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "alias-multi");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.ProxyCommand,
"ssh -J my_alias:2222 -W '[%h]:%p' localhost");
torture_unsetenv("OPENSSH_PROXYJUMP");
/* In this part, we try various other config files and strings. */
torture_setenv("OPENSSH_PROXYJUMP", "1");
@@ -2762,21 +2876,36 @@ static void torture_config_parse_uri(void **state)
(void)state; /* unused */
rc = ssh_config_parse_uri("localhost", &username, &hostname, &port, false);
rc = ssh_config_parse_uri("localhost",
&username,
&hostname,
&port,
false,
true);
assert_return_code(rc, errno);
assert_null(username);
assert_string_equal(hostname, "localhost");
SAFE_FREE(hostname);
assert_null(port);
rc = ssh_config_parse_uri("1.2.3.4", &username, &hostname, &port, false);
rc = ssh_config_parse_uri("1.2.3.4",
&username,
&hostname,
&port,
false,
true);
assert_return_code(rc, errno);
assert_null(username);
assert_string_equal(hostname, "1.2.3.4");
SAFE_FREE(hostname);
assert_null(port);
rc = ssh_config_parse_uri("1.2.3.4:2222", &username, &hostname, &port, false);
rc = ssh_config_parse_uri("1.2.3.4:2222",
&username,
&hostname,
&port,
false,
true);
assert_return_code(rc, errno);
assert_null(username);
assert_string_equal(hostname, "1.2.3.4");
@@ -2784,7 +2913,12 @@ static void torture_config_parse_uri(void **state)
assert_string_equal(port, "2222");
SAFE_FREE(port);
rc = ssh_config_parse_uri("[1:2:3::4]:2222", &username, &hostname, &port, false);
rc = ssh_config_parse_uri("[1:2:3::4]:2222",
&username,
&hostname,
&port,
false,
true);
assert_return_code(rc, errno);
assert_null(username);
assert_string_equal(hostname, "1:2:3::4");
@@ -2793,13 +2927,64 @@ static void torture_config_parse_uri(void **state)
SAFE_FREE(port);
/* do not want port */
rc = ssh_config_parse_uri("1:2:3::4", &username, &hostname, NULL, true);
rc = ssh_config_parse_uri("1:2:3::4",
&username,
&hostname,
NULL,
true,
true);
assert_return_code(rc, errno);
assert_null(username);
assert_string_equal(hostname, "1:2:3::4");
SAFE_FREE(hostname);
rc = ssh_config_parse_uri("user -name@", &username, NULL, NULL, true);
rc = ssh_config_parse_uri("user -name@", &username, NULL, NULL, true, true);
assert_int_equal(rc, SSH_ERROR);
/* Non-strict accepts non-RFC1035 chars (e.g. _, %) */
rc = ssh_config_parse_uri("customer_1",
&username,
&hostname,
NULL,
true,
false);
assert_return_code(rc, errno);
assert_null(username);
assert_string_equal(hostname, "customer_1");
SAFE_FREE(hostname);
rc = ssh_config_parse_uri("admin@%prod",
&username,
&hostname,
NULL,
true,
false);
assert_return_code(rc, errno);
assert_string_equal(username, "admin");
assert_string_equal(hostname, "%prod");
SAFE_FREE(username);
SAFE_FREE(hostname);
/* Strict rejects what non-strict accepts */
rc = ssh_config_parse_uri("customer_1",
&username,
&hostname,
NULL,
true,
true);
assert_int_equal(rc, SSH_ERROR);
/* Non-strict rejects shell metacharacters */
rc = ssh_config_parse_uri("host;cmd",
&username,
&hostname,
NULL,
true,
false);
assert_int_equal(rc, SSH_ERROR);
/* Non-strict rejects leading dash */
rc = ssh_config_parse_uri("-host", &username, &hostname, NULL, true, false);
assert_int_equal(rc, SSH_ERROR);
}
@@ -2933,6 +3118,48 @@ static void torture_config_jump(void **state)
printf("%s: EOF\n", __func__);
}
/* Verify Hostname directive resolves host without overwriting originalhost
*/
static void torture_config_hostname(void **state)
{
ssh_session session = *state;
char *expanded = NULL;
/* Hostname directive sets host, originalhost is unchanged */
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "my_alias");
assert_null(session->opts.host);
assert_string_equal(session->opts.originalhost, "my_alias");
_parse_config(session,
NULL,
"Host my_alias\n\tHostname 192.168.1.1\n",
SSH_OK);
assert_string_equal(session->opts.host, "192.168.1.1");
assert_string_equal(session->opts.originalhost, "my_alias");
/* Host keyword compares against originalhost, not the resolved IP */
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "ssh-host");
_parse_config(session,
NULL,
"Host ssh-host\n\tHostname 10.1.1.1\n"
"Host 10.1.1.*\n\tProxyJump ssh-host\n",
SSH_OK);
assert_string_equal(session->opts.host, "10.1.1.1");
assert_string_equal(session->opts.originalhost, "ssh-host");
assert_int_equal(ssh_list_count(session->opts.proxy_jumps), 0);
assert_null(session->opts.ProxyCommand);
/* %h falls back to originalhost when host is not yet resolved */
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "my_alias");
assert_null(session->opts.host);
expanded = ssh_path_expand_escape(session, "%h");
assert_non_null(expanded);
assert_string_equal(expanded, "my_alias");
free(expanded);
}
/* Invalid configuration files
*/
static void torture_config_invalid(void **state)
@@ -3101,7 +3328,8 @@ int torture_run_tests(void)
cmocka_unit_test_setup_teardown(torture_config_loglevel_missing_value,
setup,
teardown),
cmocka_unit_test_setup_teardown(torture_config_jump,
cmocka_unit_test_setup_teardown(torture_config_jump, setup, teardown),
cmocka_unit_test_setup_teardown(torture_config_hostname,
setup,
teardown),
cmocka_unit_test_setup_teardown(torture_config_invalid,

View File

@@ -17,6 +17,13 @@
#include <libssh/pki.h>
#include <libssh/pki_priv.h>
#include <libssh/session.h>
#ifdef _WIN32
#include <netioapi.h>
#else
#include <net/if.h>
#endif
#ifdef WITH_SERVER
#include <libssh/bind.h>
#define LIBSSH_CUSTOM_BIND_CONFIG_FILE "my_bind_config"
@@ -59,12 +66,16 @@ static void torture_options_set_host(void **state) {
assert_true(rc == 0);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, "localhost");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "localhost");
/* IPv4 address */
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "127.1.1.1");
assert_true(rc == 0);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, "127.1.1.1");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "127.1.1.1");
assert_null(session->opts.username);
/* IPv6 address */
@@ -72,12 +83,16 @@ static void torture_options_set_host(void **state) {
assert_true(rc == 0);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, "::1");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "::1");
assert_null(session->opts.username);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "guru@meditation");
assert_true(rc == 0);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, "meditation");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "meditation");
assert_non_null(session->opts.username);
assert_string_equal(session->opts.username, "guru");
@@ -86,6 +101,8 @@ static void torture_options_set_host(void **state) {
assert_true(rc == 0);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, "hostname");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "hostname");
assert_non_null(session->opts.username);
assert_string_equal(session->opts.username, "at@login");
@@ -104,6 +121,9 @@ static void torture_options_set_host(void **state) {
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host,
"fd4d:5449:7400:111:626d:3cff:fedf:4d39");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost,
"fd4d:5449:7400:111:626d:3cff:fedf:4d39");
assert_null(session->opts.username);
/* IPv6 hostnames should work also with square braces */
@@ -114,20 +134,103 @@ static void torture_options_set_host(void **state) {
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host,
"fd4d:5449:7400:111:626d:3cff:fedf:4d39");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost,
"fd4d:5449:7400:111:626d:3cff:fedf:4d39");
assert_null(session->opts.username);
/* user@IPv6%interface
* Use dynamic interface name for cross-platform portability */
{
char interf[IF_NAMESIZE] = {0};
char ipv6_zone[128] = {0};
char expected_host[128] = {0};
if_indextoname(1, interf);
assert_non_null(interf);
snprintf(ipv6_zone, sizeof(ipv6_zone), "user@fe80::1%%%s", interf);
snprintf(expected_host, sizeof(expected_host), "fe80::1%%%s", interf);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, ipv6_zone);
assert_return_code(rc, errno);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, expected_host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, expected_host);
assert_string_equal(session->opts.username, "user");
}
/* IDN need to be in punycode format */
SAFE_FREE(session->opts.username);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "xn--bcher-kva.tld");
assert_return_code(rc, errno);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, "xn--bcher-kva.tld");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "xn--bcher-kva.tld");
assert_null(session->opts.username);
/* IDN in UTF8 won't work */
/* IDN in UTF-8 is accepted but not as a valid hostname,
* only originalhost is set */
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "bücher.tld");
assert_string_equal(ssh_get_error(session),
"Invalid argument in ssh_options_set");
assert_return_code(rc, errno);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "bücher.tld");
/* Config alias '%' rejected by RFC1035, only originalhost is set */
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "%customer1");
assert_return_code(rc, errno);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "%customer1");
/* user@alias '_' rejected by RFC1035, alias stored in originalhost */
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "admin@customer_1");
assert_return_code(rc, errno);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "customer_1");
assert_string_equal(session->opts.username, "admin");
/* Shell metacharacters and leading dash rejected.
* Verify failure cases do not update session options. */
SAFE_FREE(session->opts.username);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "host;rm -rf /");
assert_ssh_return_code_equal(session, rc, SSH_ERROR);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "customer_1");
assert_null(session->opts.username);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "-leading-dash");
assert_ssh_return_code_equal(session, rc, SSH_ERROR);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "customer_1");
assert_null(session->opts.username);
/* Empty user or host rejected */
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "@hostname");
assert_ssh_return_code_equal(session, rc, SSH_ERROR);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "customer_1");
assert_null(session->opts.username);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "user@");
assert_ssh_return_code_equal(session, rc, SSH_ERROR);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "customer_1");
assert_null(session->opts.username);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "");
assert_ssh_return_code_equal(session, rc, SSH_ERROR);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "customer_1");
assert_null(session->opts.username);
}
static void torture_options_set_ciphers(void **state)
@@ -714,6 +817,26 @@ static void torture_options_get_host(void **state)
assert_string_equal(host, "localhost");
ssh_string_free_char(host);
/* When host is not yet resolved, falls back to originalhost */
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "my_alias");
assert_true(rc == 0);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_false(ssh_options_get(session, SSH_OPTIONS_HOST, &host));
assert_string_equal(host, "my_alias");
ssh_string_free_char(host);
/* After config resolution, get returns resolved host, not originalhost */
session->opts.host = strdup("192.168.1.1");
assert_non_null(session->opts.host);
assert_false(ssh_options_get(session, SSH_OPTIONS_HOST, &host));
assert_string_equal(host, "192.168.1.1");
ssh_string_free_char(host);
/* originalhost is unchanged */
assert_string_equal(session->opts.originalhost, "my_alias");
}
static void torture_options_set_port(void **state)
@@ -1074,6 +1197,7 @@ static void torture_options_config_host(void **state)
{
ssh_session session = *state;
FILE *config = NULL;
int rv;
/* create a new config file */
config = fopen("test_config", "w");
@@ -1113,6 +1237,33 @@ static void torture_options_config_host(void **state)
ssh_options_parse_config(session, "test_config");
assert_int_equal(session->opts.port, 44);
/* ssh_options_parse_config rejects when originalhost is NULL */
SAFE_FREE(session->opts.host);
SAFE_FREE(session->opts.originalhost);
rv = ssh_options_parse_config(session, "test_config");
assert_int_equal(rv, -1);
/* Config Hostname with invalid hostname: verify stale host not leaked */
torture_write_file("test_config", "Host 192.168.1.1\nHostname my_alias\n");
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "192.168.1.1");
assert_string_equal(session->opts.host, "192.168.1.1");
assert_string_equal(session->opts.originalhost, "192.168.1.1");
rv = ssh_options_parse_config(session, "test_config");
assert_int_equal(rv, 0);
assert_null(session->opts.host);
assert_string_equal(session->opts.originalhost, "192.168.1.1");
/* Calling ssh_options_set(HOST) twice: verify stale host not leaked */
ssh_options_set(session, SSH_OPTIONS_HOST, "real.server.com");
assert_string_equal(session->opts.host, "real.server.com");
assert_string_equal(session->opts.originalhost, "real.server.com");
ssh_options_set(session, SSH_OPTIONS_HOST, "my_alias");
assert_null(session->opts.host);
assert_string_equal(session->opts.originalhost, "my_alias");
unlink("test_config");
}
@@ -1437,6 +1588,7 @@ static void torture_options_copy(void **state)
assert_string_equal(session->opts.username, new->opts.username);
assert_string_equal(session->opts.host, new->opts.host);
assert_string_equal(session->opts.originalhost, new->opts.originalhost);
assert_string_equal(session->opts.bindaddr, new->opts.bindaddr);
assert_string_equal(session->opts.sshdir, new->opts.sshdir);
assert_string_equal(session->opts.knownhosts, new->opts.knownhosts);

View File

@@ -421,3 +421,95 @@
fun:gss_acquire_cred_from
fun:gss_acquire_cred
}
# Function mecherror_copy called in various
# functions of the krb5 library copies entries
# to the global error mapping table (mecherrmap m).
{
Global error mapping table in krb5
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
fun:mecherror_copy
}
# Function add_error_table called in various
# functions of the krb5 library adds entries
# to a global list of error tables et_list.
{
Global list of error tables in krb5
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
fun:add_error_table
}
# Function build_mechSet builds the global
# gss_OID_set_desc g_mechSet which is only
# free'd when initialized again.
{
Global OID set in krb5
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:build_mechSet
}
# Function gssint_register_mechinfo()
# called from gssint_mechglue_init() adds
# entries to a global linked list g_mechList.
{
Global list of gss_mech_info in krb5
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:gssint_register_mechinfo*
...
fun:gssint_mechglue_init
}
# Function addConfigEntry() called during
# updateMechList() adds entries to
# a global linked list g_mechList.
{
Global list of gss_mech_info in krb5
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:addConfigEntry
...
fun:updateMechList
}
# Function loadInterMech() called during
# updateMechList() loops through the global
# linked list g_mechList and updates its entries
# with heap-alloced "interposer fields".
{
Global list of gss_mech_info in krb5
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:loadInterMech
...
fun:updateMechList
}
# Multiple krb5 functions call krb5int_open_plugin
# which opens shared libraries using dlopen.
# The plugin handle then seems to be stored in the
# main krb5 context.
{
Plugin handles stored in the krb5 context
Memcheck:Leak
match-leak-kinds: reachable
fun:malloc
...
fun:dlopen*
...
fun:krb5int_open_plugin
}