Repository: libssh2/libssh2 (c)

Issue:
Agent forwarding
Is there any interest in implementing agent forwarding functionality in libssh2? At one point a patch was submitted (https://www.libssh2.org/mail/libssh2-devel-archive-2012-03/0114.shtml) but it looks like it wasn't merged in (https://www.libssh2.org/mail/libssh2-devel-archive-2012-03/0121.shtml).


Discussion:

Patch:
diff --git a/docs/libssh2_channel_request_auth_agent.3 b/docs/libssh2_channel_request_auth_agent.3
new file mode 100644
--- /dev/null
+++ b/docs/libssh2_channel_request_auth_agent.3
@@ -0,0 +1,22 @@
+.TH libssh2_channel_request_auth_agent 3 "1 Jun 2007" "libssh2 0.15" "libssh2 manual"
+.SH NAME
+libssh2_channel_request_auth_agent - request agent forwarding for a session
+.SH SYNOPSIS
+#include <libssh2.h>
+
+int
+libssh2_channel_request_auth_agent(LIBSSH2_CHANNEL *channel);
+
+.SH DESCRIPTION
+Request that agent forwarding be enabled for this SSH session. This sends the
+request over this specific channel, which causes the agent listener to be
+started on the remote side upon success. This agent listener will then run
+for the duration of the SSH session.
+
+\fIchannel\fP - Previously opened channel instance such as returned by
+.BR libssh2_channel_open_ex(3)
+
+.SH RETURN VALUE
+Return 0 on success or negative on failure.  It returns
+LIBSSH2_ERROR_EAGAIN when it would otherwise block. While
+LIBSSH2_ERROR_EAGAIN is a negative number, it isn't really a failure per se.
diff --git a/example/.gitignore b/example/.gitignore
--- a/example/.gitignore
+++ b/example/.gitignore
@@ -20,6 +20,7 @@ sftp_write_nonblock
 config.h.in
 ssh2_exec
 ssh2_agent
+ssh2_agent_forwarding
 libssh2_config.h
 libssh2_config.h.in
 stamp-h2
diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt
--- a/example/CMakeLists.txt
+++ b/example/CMakeLists.txt
@@ -57,6 +57,7 @@ set(EXAMPLES
   sftpdir_nonblock
   ssh2_exec
   ssh2_agent
+  ssh2_agent_forwarding
   ssh2_echo
   sftp_append
   subsystem_netconf
diff --git a/example/Makefile.am b/example/Makefile.am
--- a/example/Makefile.am
+++ b/example/Makefile.am
@@ -6,8 +6,8 @@ EXTRA_DIST = libssh2_config.h.in libssh2_config_cmake.h.in CMakeLists.txt
 noinst_PROGRAMS = direct_tcpip ssh2 scp scp_nonblock scp_write		\
  scp_write_nonblock sftp sftp_nonblock sftp_write sftp_write_nonblock	\
  sftp_mkdir sftp_mkdir_nonblock sftp_RW_nonblock sftp_write_sliding	\
- sftpdir sftpdir_nonblock ssh2_exec ssh2_agent ssh2_echo sftp_append	\
- subsystem_netconf tcpip-forward
+ sftpdir sftpdir_nonblock ssh2_exec ssh2_agent ssh2_agent_forwarding \
+ ssh2_echo sftp_append subsystem_netconf tcpip-forward
 
 if HAVE_SYS_UN_H
 noinst_PROGRAMS += x11
diff --git a/example/ssh2_agent_forwarding.c b/example/ssh2_agent_forwarding.c
new file mode 100644
--- /dev/null
+++ b/example/ssh2_agent_forwarding.c
@@ -0,0 +1,292 @@
+/*
+ * Sample showing how to use libssh2 to request agent forwarding
+ * on the remote host. The command executed will run with agent forwarded
+ * so you should be able to do things like clone out protected git
+ * repos and such.
+ *
+ * The example uses agent authentication to ensure an agent to forward
+ * is running.
+ *
+ * Run it like this:
+ *
+ * $ ./ssh2_agent_forwarding 127.0.0.1 user "uptime"
+ *
+ */
+
+#include "libssh2_config.h"
+#include <libssh2.h>
+
+#ifdef HAVE_WINSOCK2_H
+# include <winsock2.h>
+#endif
+#ifdef HAVE_SYS_SOCKET_H
+# include <sys/socket.h>
+#endif
+#ifdef HAVE_NETINET_IN_H
+# include <netinet/in.h>
+#endif
+#ifdef HAVE_SYS_SELECT_H
+# include <sys/select.h>
+#endif
+# ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+#ifdef HAVE_ARPA_INET_H
+# include <arpa/inet.h>
+#endif
+
+#ifdef HAVE_SYS_TIME_H
+# include <sys/time.h>
+#endif
+#include <sys/types.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <stdio.h>
+#include <ctype.h>
+
+static int waitsocket(int socket_fd, LIBSSH2_SESSION *session)
+{
+    struct timeval timeout;
+    int rc;
+    fd_set fd;
+    fd_set *writefd = NULL;
+    fd_set *readfd = NULL;
+    int dir;
+
+    timeout.tv_sec = 10;
+    timeout.tv_usec = 0;
+
+    FD_ZERO(&fd);
+
+    FD_SET(socket_fd, &fd);
+
+    /* now make sure we wait in the correct direction */
+    dir = libssh2_session_block_directions(session);
+
+    if(dir & LIBSSH2_SESSION_BLOCK_INBOUND)
+        readfd = &fd;
+
+    if(dir & LIBSSH2_SESSION_BLOCK_OUTBOUND)
+        writefd = &fd;
+
+    rc = select(socket_fd + 1, readfd, writefd, NULL, &timeout);
+
+    return rc;
+}
+
+int main(int argc, char *argv[])
+{
+    const char *hostname = "127.0.0.1";
+    const char *commandline = "uptime";
+    const char *username    = NULL;
+    unsigned long hostaddr;
+    int sock;
+    struct sockaddr_in sin;
+    LIBSSH2_SESSION *session;
+    LIBSSH2_CHANNEL *channel;
+    LIBSSH2_AGENT *agent = NULL;
+    struct libssh2_agent_publickey *identity, *prev_identity = NULL;
+    int rc;
+    int exitcode;
+    char *exitsignal = (char *)"none";
+    int bytecount = 0;
+
+#ifdef WIN32
+    WSADATA wsadata;
+    WSAStartup(MAKEWORD(2, 0), &wsadata);
+#endif
+    if(argc < 2) {
+        fprintf(stderr, "At least IP and username arguments are required.\n");
+        return 1;
+    }
+    /* must be ip address only */
+    hostname = argv[1];
+    username = argv[2];
+
+    if(argc > 3) {
+        commandline = argv[3];
+    }
+
+    rc = libssh2_init(0);
+    if(rc != 0) {
+        fprintf(stderr, "libssh2 initialization failed (%d)\n", rc);
+        return 1;
+    }
+
+    hostaddr = inet_addr(hostname);
+
+    /* Ultra basic "connect to port 22 on localhost"
+     * Your code is responsible for creating the socket establishing the
+     * connection
+     */
+    sock = socket(AF_INET, SOCK_STREAM, 0);
+
+    sin.sin_family = AF_INET;
+    sin.sin_port = htons(22);
+    sin.sin_addr.s_addr = hostaddr;
+    if(connect(sock, (struct sockaddr*)(&sin),
+                sizeof(struct sockaddr_in)) != 0) {
+        fprintf(stderr, "failed to connect!\n");
+        return -1;
+    }
+
+    /* Create a session instance */
+    session = libssh2_session_init();
+    if(!session)
+        return -1;
+
+    if(libssh2_session_handshake(session, sock) != 0) {
+        fprintf(stderr, "Failure establishing SSH session: %d\n", rc);
+        return -1;
+    }
+
+    /* Connect to the ssh-agent */
+    agent = libssh2_agent_init(session);
+    if(!agent) {
+        fprintf(stderr, "Failure initializing ssh-agent support\n");
+        rc = 1;
+        goto shutdown;
+    }
+    if(libssh2_agent_connect(agent)) {
+        fprintf(stderr, "Failure connecting to ssh-agent\n");
+        rc = 1;
+        goto shutdown;
+    }
+    if(libssh2_agent_list_identities(agent)) {
+        fprintf(stderr, "Failure requesting identities to ssh-agent\n");
+        rc = 1;
+        goto shutdown;
+    }
+    while(1) {
+        rc = libssh2_agent_get_identity(agent, &identity, prev_identity);
+        if(rc == 1)
+            break;
+        if(rc < 0) {
+            fprintf(stderr,
+                    "Failure obtaining identity from ssh-agent support\n");
+            rc = 1;
+            goto shutdown;
+        }
+        if(libssh2_agent_userauth(agent, username, identity)) {
+            fprintf(stderr, "\tAuthentication with username %s and "
+                   "public key %s failed!\n",
+                   username, identity->comment);
+        }
+        else {
+            fprintf(stderr, "\tAuthentication with username %s and "
+                   "public key %s succeeded!\n",
+                   username, identity->comment);
+            break;
+        }
+        prev_identity = identity;
+    }
+    if(rc) {
+        fprintf(stderr, "Couldn't continue authentication\n");
+        goto shutdown;
+    }
+
+#if 0
+    libssh2_trace(session, ~0);
+#endif
+
+    /* Set session to non-blocking */
+    libssh2_session_set_blocking(session, 0);
+
+    /* Exec non-blocking on the remove host */
+    while((channel = libssh2_channel_open_session(session)) == NULL &&
+          libssh2_session_last_error(session, NULL, NULL, 0) ==
+          LIBSSH2_ERROR_EAGAIN) {
+        waitsocket(sock, session);
+    }
+    if(channel == NULL) {
+        fprintf(stderr, "Error\n");
+        exit(1);
+    }
+    while((rc = libssh2_channel_request_auth_agent(channel)) ==
+          LIBSSH2_ERROR_EAGAIN) {
+        waitsocket(sock, session);
+    }
+    if(rc != 0) {
+        fprintf(stderr, "Error, couldn't request auth agent, error code %d.\n",
+                rc);
+        exit(1);
+    }
+    else {
+        fprintf(stdout, "\tAgent forwarding request succeeded!\n");
+    }
+    while((rc = libssh2_channel_exec(channel, commandline)) ==
+          LIBSSH2_ERROR_EAGAIN) {
+        waitsocket(sock, session);
+    }
+    if(rc != 0) {
+        fprintf(stderr, "Error\n");
+        exit(1);
+    }
+    for(;;) {
+        /* loop until we block */
+        int rc;
+        do {
+            char buffer[0x4000];
+            rc = libssh2_channel_read(channel, buffer, sizeof(buffer) );
+            if(rc > 0) {
+                int i;
+                bytecount += rc;
+                fprintf(stderr, "We read:\n");
+                for(i = 0; i < rc; ++i)
+                    fputc(buffer[i], stderr);
+                fprintf(stderr, "\n");
+            }
+            else {
+                if(rc != LIBSSH2_ERROR_EAGAIN)
+                    /* no need to output this for the EAGAIN case */
+                    fprintf(stderr, "libssh2_channel_read returned %d\n", rc);
+            }
+        }
+        while(rc > 0);
+
+        /* this is due to blocking that would occur otherwise so we loop on
+           this condition */
+        if(rc == LIBSSH2_ERROR_EAGAIN) {
+            waitsocket(sock, session);
+        }
+        else
+            break;
+    }
+    exitcode = 127;
+    while((rc = libssh2_channel_close(channel)) == LIBSSH2_ERROR_EAGAIN) {
+        waitsocket(sock, session);
+    }
+    if(rc == 0) {
+        exitcode = libssh2_channel_get_exit_status(channel);
+        libssh2_channel_get_exit_signal(channel, &exitsignal,
+                                        NULL, NULL, NULL, NULL, NULL);
+    }
+
+    if(exitsignal) {
+        printf("\nGot signal: %s\n", exitsignal);
+    }
+    else {
+        printf("\nEXIT: %d bytecount: %d\n", exitcode, bytecount);
+    }
+
+    libssh2_channel_free(channel);
+    channel = NULL;
+
+shutdown:
+
+    libssh2_session_disconnect(session,
+                               "Normal Shutdown, Thank you for playing");
+    libssh2_session_free(session);
+
+#ifdef WIN32
+    closesocket(sock);
+#else
+    close(sock);
+#endif
+    fprintf(stderr, "all done\n");
+
+    libssh2_exit();
+
+    return 0;
+}
diff --git a/include/libssh2.h b/include/libssh2.h
--- a/include/libssh2.h
+++ b/include/libssh2.h
@@ -761,6 +761,8 @@ LIBSSH2_API int libssh2_channel_setenv_ex(LIBSSH2_CHANNEL *channel,
                               (unsigned int)strlen(varname), (value),   \
                               (unsigned int)strlen(value))
 
+LIBSSH2_API int libssh2_channel_request_auth_agent(LIBSSH2_CHANNEL *channel);
+
 LIBSSH2_API int libssh2_channel_request_pty_ex(LIBSSH2_CHANNEL *channel,
                                                const char *term,
                                                unsigned int term_len,
diff --git a/src/channel.c b/src/channel.c
--- a/src/channel.c
+++ b/src/channel.c
@@ -1021,6 +1021,158 @@ static int channel_request_pty(LIBSSH2_CHANNEL *channel,
                           "channel request-pty");
 }
 
+/**
+ * channel_request_auth_agent
+ * The actual re-entrant method which requests an auth agent.
+ * */
+static int channel_request_auth_agent(LIBSSH2_CHANNEL *channel,
+                                      const char *request_str,
+                                      int request_str_len)
+{
+    LIBSSH2_SESSION *session = channel->session;
+    unsigned char *s;
+    static const unsigned char reply_codes[3] =
+        { SSH_MSG_CHANNEL_SUCCESS, SSH_MSG_CHANNEL_FAILURE, 0 };
+    int rc;
+
+    if(channel->req_auth_agent_state == libssh2_NB_state_idle) {
+        /* Only valid options are "auth-agent-req" and
+         * "auth-agent-req_at_openssh.com" so we make sure it is not
+         * actually longer than the longest possible. */
+        if(request_str_len > 26) {
+            return _libssh2_error(session, LIBSSH2_ERROR_INVAL,
+                                  "request_str length too large");
+        }
+
+        /*
+         *  Length: 24 or 36 = packet_type(1) + channel(4) + req_len(4) +
+         *    request_str (variable) + want_reply (1) */
+        channel->req_auth_agent_packet_len = 10 + request_str_len;
+
+        /* Zero out the requireev state to reset */
+        memset(&channel->req_auth_agent_requirev_state, 0,
+               sizeof(channel->req_auth_agent_requirev_state));
+
+        _libssh2_debug(session, LIBSSH2_TRACE_CONN,
+                       "Requesting auth agent on channel %lu/%lu",
+                       channel->local.id, channel->remote.id);
+
+        /*
+         *  byte      SSH_MSG_CHANNEL_REQUEST
+         *  uint32    recipient channel
+         *  string    "auth-agent-req"
+         *  boolean   want reply
+         * */
+        s = channel->req_auth_agent_packet;
+        *(s++) = SSH_MSG_CHANNEL_REQUEST;
+        _libssh2_store_u32(&s, channel->remote.id);
+        _libssh2_store_str(&s, (char *)request_str, request_str_len);
+        *(s++) = 0x01;
+
+        channel->req_auth_agent_state = libssh2_NB_state_created;
+    }
+
+    if(channel->req_auth_agent_state == libssh2_NB_state_created) {
+        /* Send the packet, we can use sizeof() on the packet because it
+         * is always completely filled; there are no variable length fields. */
+        rc = _libssh2_transport_send(session, channel->req_auth_agent_packet,
+                                     channel->req_auth_agent_packet_len,
+                                     NULL, 0);
+
+        if(rc == LIBSSH2_ERROR_EAGAIN) {
+            _libssh2_error(session, rc,
+                           "Would block sending auth-agent request");
+        }
+        else if(rc) {
+            channel->req_auth_agent_state = libssh2_NB_state_idle;
+            return _libssh2_error(session, rc,
+                                  "Unable to send auth-agent request");
+        }
+        _libssh2_htonu32(channel->req_auth_agent_local_channel,
+                         channel->local.id);
+        channel->req_auth_agent_state = libssh2_NB_state_sent;
+    }
+
+    if(channel->req_auth_agent_state == libssh2_NB_state_sent) {
+        unsigned char *data;
+        size_t data_len;
+        unsigned char code;
+
+        rc = _libssh2_packet_requirev(
+            session, reply_codes, &data, &data_len, 1,
+            channel->req_auth_agent_local_channel,
+            4, &channel->req_auth_agent_requirev_state);
+        if(rc == LIBSSH2_ERROR_EAGAIN) {
+            return rc;
+        }
+        else if(rc) {
+            channel->req_auth_agent_state = libssh2_NB_state_idle;
+            return _libssh2_error(session, LIBSSH2_ERROR_PROTO,
+                                  "Failed to request auth-agent");
+        }
+
+        code = data[0];
+
+        LIBSSH2_FREE(session, data);
+        channel->req_auth_agent_state = libssh2_NB_state_idle;
+
+        if(code == SSH_MSG_CHANNEL_SUCCESS)
+            return 0;
+    }
+
+    return _libssh2_error(session, LIBSSH2_ERROR_CHANNEL_REQUEST_DENIED,
+                          "Unable to complete request for auth-agent");
+}
+
+/**
+ * libssh2_channel_request_auth_agent
+ * Requests that agent forwarding be enabled for the session. The
+ * request must be sent over a specific channel, which starts the agent
+ * listener on the remote side. Once the channel is closed, the agent
+ * listener continues to exist.
+ * */
+LIBSSH2_API int
+libssh2_channel_request_auth_agent(LIBSSH2_CHANNEL *channel)
+{
+    int rc;
+
+    if(!channel)
+        return LIBSSH2_ERROR_BAD_USE;
+
+    /* The current RFC draft for agent forwarding says you're supposed to
+     * send "auth-agent-req," but most SSH servers out there right now
+     * actually expect "auth-agent-req@openssh.com", so we try that
+     * first. */
+    if(channel->req_auth_agent_try_state == libssh2_NB_state_idle) {
+        BLOCK_ADJUST(rc, channel->session,
+                     channel_request_auth_agent(channel,
+                                                "auth-agent-req@openssh.com",
+                                                26));
+
+        /* If we failed (but not with EAGAIN), then we move onto
+         * the next step to try another request type. */
+        if(rc != 0 && rc != LIBSSH2_ERROR_EAGAIN)
+            channel->req_auth_agent_try_state = libssh2_NB_state_sent;
+    }
+
+    if(channel->req_auth_agent_try_state == libssh2_NB_state_sent) {
+        BLOCK_ADJUST(rc, channel->session,
+                     channel_request_auth_agent(channel,
+                                                "auth-agent-req", 14));
+
+        /* If we failed without an EAGAIN, then move on with this
+         * state machine. */
+        if(rc != 0 && rc != LIBSSH2_ERROR_EAGAIN)
+            channel->req_auth_agent_try_state = libssh2_NB_state_sent1;
+    }
+
+    /* If things are good, reset the try state. */
+    if(rc == 0)
+        channel->req_auth_agent_try_state = libssh2_NB_state_idle;
+
+    return rc;
+}
+
 /*
  * libssh2_channel_request_pty_ex
  * Duh... Request a PTY
diff --git a/src/libssh2_priv.h b/src/libssh2_priv.h
--- a/src/libssh2_priv.h
+++ b/src/libssh2_priv.h
@@ -452,6 +452,13 @@ struct _LIBSSH2_CHANNEL
     /* State variables used in libssh2_channel_handle_extended_data2() */
     libssh2_nonblocking_states extData2_state;
 
+    /* State variables used in libssh2_channel_request_auth_agent() */
+    libssh2_nonblocking_states req_auth_agent_try_state;
+    libssh2_nonblocking_states req_auth_agent_state;
+    unsigned char req_auth_agent_packet[36];
+    size_t req_auth_agent_packet_len;
+    unsigned char req_auth_agent_local_channel[4];
+    packet_requirev_state_t req_auth_agent_requirev_state;
 };
 
 struct _LIBSSH2_LISTENER


Test patch:
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -118,6 +118,7 @@ set(TESTS
   public_key_auth_succeeds_with_correct_encrypted_rsa_key
   keyboard_interactive_auth_fails_with_wrong_response
   keyboard_interactive_auth_succeeds_with_correct_response
+  agent_forward_succeeds
   )
 
 if(CRYPTO_BACKEND STREQUAL "OpenSSL")
diff --git a/tests/test_agent_forward_succeeds.c b/tests/test_agent_forward_succeeds.c
new file mode 100644
--- /dev/null
+++ b/tests/test_agent_forward_succeeds.c
@@ -0,0 +1,51 @@
+#include "session_fixture.h"
+
+#include <libssh2.h>
+
+#include <stdio.h>
+
+const char *USERNAME = "libssh2"; /* configured in Dockerfile */
+const char *KEY_FILE_PRIVATE = "key_rsa";
+const char *KEY_FILE_PUBLIC = "key_rsa.pub"; /* configured in Dockerfile */
+
+int test(LIBSSH2_SESSION *session)
+{
+    int rc;
+    LIBSSH2_CHANNEL *channel;
+
+    const char *userauth_list =
+        libssh2_userauth_list(session, USERNAME, strlen(USERNAME));
+    if (userauth_list == NULL) {
+        print_last_session_error("libssh2_userauth_list");
+        return 1;
+    }
+
+    if (strstr(userauth_list, "publickey") == NULL) {
+        fprintf(stderr, "'publickey' was expected in userauth list: %s\n",
+                userauth_list);
+        return 1;
+    }
+
+    rc = libssh2_userauth_publickey_fromfile_ex(
+        session, USERNAME, strlen(USERNAME), KEY_FILE_PUBLIC, KEY_FILE_PRIVATE,
+        NULL);
+    if (rc != 0) {
+        print_last_session_error("libssh2_userauth_publickey_fromfile_ex");
+        return 1;
+    }
+
+    channel = libssh2_channel_open_session(session);
+    /* if (channel == NULL) { */
+    /*     printf("Error opening channel\n"); */
+    /*     return 1; */
+    /* } */
+
+    rc = libssh2_channel_request_auth_agent(channel);
+    if (rc != 0) {
+        fprintf(stderr, "Auth agent request for agent forwarding failed, error code %d\n",
+                rc);
+        return 1;
+    }
+
+    return 0;
+}