From 8782fcec18bbcaf8b6f8eb936a9e1cf3093902e6 Mon Sep 17 00:00:00 2001 From: Madhav Vasisth Date: Sat, 14 Feb 2026 12:06:25 +0000 Subject: [PATCH] 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 Reviewed-by: Jakub Jelen --- include/libssh/agent.h | 11 ++ src/agent.c | 88 +++++++++++++++ tests/client/torture_auth_agent_forwarding.c | 108 ++++++++++++++++++- 3 files changed, 204 insertions(+), 3 deletions(-) diff --git a/include/libssh/agent.h b/include/libssh/agent.h index 5f1af39a..371198cb 100644 --- a/include/libssh/agent.h +++ b/include/libssh/agent.h @@ -176,6 +176,17 @@ ssh_key ssh_agent_get_next_ident(struct ssh_session_struct *session, 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 diff --git a/src/agent.c b/src/agent.c index bb1669c3..eb20177b 100644 --- a/src/agent.c +++ b/src/agent.c @@ -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; +} diff --git a/tests/client/torture_auth_agent_forwarding.c b/tests/client/torture_auth_agent_forwarding.c index cdde5328..3fc67ed7 100644 --- a/tests/client/torture_auth_agent_forwarding.c +++ b/tests/client/torture_auth_agent_forwarding.c @@ -10,6 +10,7 @@ #include "libssh/callbacks.h" #include "libssh/libssh.h" +#include #include "libssh/priv.h" #include @@ -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