Merge pull request #648 from terrafrost/agentforwarding-2.0-1

Agent Forwarding for 2.0 branch

* terrafrost/agentforwarding-2.0-1:
  SSH2: missed a file in the merge
  removed unwarrented user_error
  preference isset over array_key_exists, return false on failure, break after return channel opened
  moved agent forwarding channel handling to filter method and reusing existing open channels to request forwarding
  removed stopSSHForwarding
  determining what failure to expect
  addresses low hanging fruit comments from terrafrost and bantu
  removed superfluous default case
  SSH agent forwarding implementation
This commit is contained in:
Andreas Fischer 2015-03-30 12:23:10 +02:00
commit 28a2f0fc0c
4 changed files with 300 additions and 49 deletions

View File

@ -100,9 +100,10 @@ class SSH2
* @see \phpseclib\Net\SSH2::_get_channel_packet() * @see \phpseclib\Net\SSH2::_get_channel_packet()
* @access private * @access private
*/ */
const CHANNEL_EXEC = 0; // PuTTy uses 0x100 const CHANNEL_EXEC = 0; // PuTTy uses 0x100
const CHANNEL_SHELL = 1; const CHANNEL_SHELL = 1;
const CHANNEL_SUBSYSTEM = 2; const CHANNEL_SUBSYSTEM = 2;
const CHANNEL_AGENT_FORWARD = 3;
/**#@-*/ /**#@-*/
/**#@+ /**#@+
@ -835,6 +836,14 @@ class SSH2
*/ */
var $windowRows = 24; var $windowRows = 24;
/**
* A System_SSH_Agent for use in the SSH2 Agent Forwarding scenario
*
* @var System_SSH_Agent
* @access private
*/
var $agent;
/** /**
* Default Constructor. * Default Constructor.
* *
@ -2055,6 +2064,7 @@ class SSH2
*/ */
function _ssh_agent_login($username, $agent) function _ssh_agent_login($username, $agent)
{ {
$this->agent = $agent;
$keys = $agent->requestIdentities(); $keys = $agent->requestIdentities();
foreach ($keys as $key) { foreach ($keys as $key) {
if ($this->_privatekey_login($username, $key)) { if ($this->_privatekey_login($username, $key)) {
@ -2234,6 +2244,7 @@ class SSH2
if (!$this->_send_binary_packet($packet)) { if (!$this->_send_binary_packet($packet)) {
return false; return false;
} }
$response = $this->_get_binary_packet(); $response = $this->_get_binary_packet();
if ($response === false) { if ($response === false) {
user_error('Connection closed by server'); user_error('Connection closed by server');
@ -2400,6 +2411,24 @@ class SSH2
} }
} }
/**
* Return an available open channel
*
* @return Integer
* @access public
*/
function _get_open_channel()
{
$channel = self::CHANNEL_EXEC;
do {
if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_OPEN) {
return $channel;
}
} while ($channel++ < self::CHANNEL_SUBSYSTEM);
return false;
}
/** /**
* Returns the output of an interactive shell * Returns the output of an interactive shell
* *
@ -2758,18 +2787,41 @@ class SSH2
case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1 case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1
$this->_string_shift($payload, 1); $this->_string_shift($payload, 1);
extract(unpack('Nlength', $this->_string_shift($payload, 4))); extract(unpack('Nlength', $this->_string_shift($payload, 4)));
$this->errors[] = 'SSH_MSG_CHANNEL_OPEN: ' . utf8_decode($this->_string_shift($payload, $length)); $data = $this->_string_shift($payload, $length);
$this->_string_shift($payload, 4); // skip over client channel
extract(unpack('Nserver_channel', $this->_string_shift($payload, 4))); extract(unpack('Nserver_channel', $this->_string_shift($payload, 4)));
switch($data) {
case 'auth-agent':
case 'auth-agent@openssh.com':
if (isset($this->agent)) {
$new_channel = self::CHANNEL_AGENT_FORWARD;
$packet = pack('CN3a*Na*', extract(unpack('Nremote_window_size', $this->_string_shift($payload, 4)));
NET_SSH2_MSG_REQUEST_FAILURE, $server_channel, NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, 0, '', 0, ''); extract(unpack('Nremote_maximum_packet_size', $this->_string_shift($payload, 4)));
if (!$this->_send_binary_packet($packet)) { $this->packet_size_client_to_server[$new_channel] = $remote_window_size;
return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); $this->window_size_server_to_client[$new_channel] = $remote_maximum_packet_size;
$this->window_size_client_to_server[$new_channel] = $this->window_size;
$packet_size = 0x4000;
$packet = pack('CN4',
NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION, $server_channel, $new_channel, $packet_size, $packet_size);
$this->server_channels[$new_channel] = $server_channel;
$this->channel_status[$new_channel] = NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION;
if (!$this->_send_binary_packet($packet)) {
return false;
}
}
break;
default:
$packet = pack('CN3a*Na*',
NET_SSH2_MSG_REQUEST_FAILURE, $server_channel, NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, 0, '', 0, '');
if (!$this->_send_binary_packet($packet)) {
return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
}
} }
$payload = $this->_get_binary_packet(); $payload = $this->_get_binary_packet();
break; break;
case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST: case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST:
@ -2906,48 +2958,59 @@ class SSH2
return ''; return '';
} }
extract(unpack('Ctype/Nchannel', $this->_string_shift($response, 5))); extract(unpack('Ctype', $this->_string_shift($response, 1)));
$this->window_size_server_to_client[$channel]-= strlen($response); if ($type == NET_SSH2_MSG_CHANNEL_OPEN) {
extract(unpack('Nlength', $this->_string_shift($response, 4)));
// resize the window, if appropriate } else {
if ($this->window_size_server_to_client[$channel] < 0) { extract(unpack('Nchannel', $this->_string_shift($response, 4)));
$packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_size);
if (!$this->_send_binary_packet($packet)) {
return false;
}
$this->window_size_server_to_client[$channel]+= $this->window_size;
} }
switch ($this->channel_status[$channel]) { // will not be setup yet on incoming channel open request
case NET_SSH2_MSG_CHANNEL_OPEN: if (isset($channel) && isset($this->channel_status[$channel]) && isset($this->window_size_server_to_client[$channel])) {
switch ($type) { $this->window_size_server_to_client[$channel]-= strlen($response);
case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION:
extract(unpack('Nserver_channel', $this->_string_shift($response, 4))); // resize the window, if appropriate
$this->server_channels[$channel] = $server_channel; if ($this->window_size_server_to_client[$channel] < 0) {
extract(unpack('Nwindow_size', $this->_string_shift($response, 4))); $packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_size);
$this->window_size_client_to_server[$channel] = $window_size; if (!$this->_send_binary_packet($packet)) {
$temp = unpack('Npacket_size_client_to_server', $this->_string_shift($response, 4)); return false;
$this->packet_size_client_to_server[$channel] = $temp['packet_size_client_to_server'];
return $client_channel == $channel ? true : $this->_get_channel_packet($client_channel, $skip_extended);
//case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE:
default:
user_error('Unable to open channel');
return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
} }
break; $this->window_size_server_to_client[$channel]+= $this->window_size;
case NET_SSH2_MSG_CHANNEL_REQUEST: }
switch ($type) {
case NET_SSH2_MSG_CHANNEL_SUCCESS: switch ($this->channel_status[$channel]) {
return true; case NET_SSH2_MSG_CHANNEL_OPEN:
case NET_SSH2_MSG_CHANNEL_FAILURE: switch ($type) {
return false; case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION:
default: extract(unpack('Nserver_channel', $this->_string_shift($response, 4)));
user_error('Unable to fulfill channel request'); $this->server_channels[$channel] = $server_channel;
return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); extract(unpack('Nwindow_size', $this->_string_shift($response, 4)));
} $this->window_size_client_to_server[$channel] = $window_size;
case NET_SSH2_MSG_CHANNEL_CLOSE: $temp = unpack('Npacket_size_client_to_server', $this->_string_shift($response, 4));
return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->_get_channel_packet($client_channel, $skip_extended); $this->packet_size_client_to_server[$channel] = $temp['packet_size_client_to_server'];
$result = $client_channel == $channel ? true : $this->_get_channel_packet($client_channel, $skip_extended);
$this->_on_channel_open();
return $result;
//case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE:
default:
user_error('Unable to open channel');
return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
}
break;
case NET_SSH2_MSG_CHANNEL_REQUEST:
switch ($type) {
case NET_SSH2_MSG_CHANNEL_SUCCESS:
return true;
case NET_SSH2_MSG_CHANNEL_FAILURE:
return false;
default:
user_error('Unable to fulfill channel request');
return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
}
case NET_SSH2_MSG_CHANNEL_CLOSE:
return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->_get_channel_packet($client_channel, $skip_extended);
}
} }
// ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA // ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA
@ -2965,6 +3028,15 @@ class SSH2
*/ */
extract(unpack('Nlength', $this->_string_shift($response, 4))); extract(unpack('Nlength', $this->_string_shift($response, 4)));
$data = $this->_string_shift($response, $length); $data = $this->_string_shift($response, $length);
if ($channel == self::CHANNEL_AGENT_FORWARD) {
$agent_response = $this->agent->_forward_data($data);
if (!is_bool($agent_response)) {
$this->_send_channel_packet($channel, $agent_response);
}
break;
}
if ($client_channel == $channel) { if ($client_channel == $channel) {
return $data; return $data;
} }
@ -3401,6 +3473,22 @@ class SSH2
return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT); return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT);
} }
/**
* Helper function for agent->_on_channel_open()
*
* Used when channels are created to inform agent
* of said channel opening. Must be called after
* channel open confirmation received
*
* @access private
*/
function _on_channel_open()
{
if (isset($this->agent)) {
$this->agent->_on_channel_open($this);
}
}
/** /**
* Returns all errors * Returns all errors
* *

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Pure-PHP ssh-agent client. * Pure-PHP ssh-agent client.
* *
@ -61,6 +62,19 @@ class Agent
const SSH_AGENT_SIGN_RESPONSE = 14; const SSH_AGENT_SIGN_RESPONSE = 14;
/**#@-*/ /**#@-*/
/**@+
* Agent forwarding status
*
* @access private
*/
// no forwarding requested and not active
const FORWARD_NONE = 0;
// request agent forwarding when opportune
const FORWARD_REQUEST = 1;
// forwarding has been request and is active
const FORWARD_ACTIVE = 2;
/**#@-*/
/** /**
* Unused * Unused
*/ */
@ -74,6 +88,29 @@ class Agent
*/ */
var $fsock; var $fsock;
/**
* Agent forwarding status
*
* @access private
*/
var $forward_status = self::FORWARD_NONE;
/**
* Buffer for accumulating forwarded authentication
* agent data arriving on SSH data channel destined
* for agent unix socket
*
* @access private
*/
var $socket_buffer = '';
/**
* Tracking the number of bytes we are expecting
* to arrive for the agent socket on the SSH data
* channel
*/
var $expected_bytes = 0;
/** /**
* Default Constructor * Default Constructor
* *
@ -156,4 +193,107 @@ class Agent
return $identities; return $identities;
} }
/**
* Signal that agent forwarding should
* be requested when a channel is opened
*
* @param Net_SSH2 $ssh
* @return Boolean
* @access public
*/
function startSSHForwarding($ssh)
{
if ($this->forward_status == self::FORWARD_NONE) {
$this->forward_status = self::FORWARD_REQUEST;
}
}
/**
* Request agent forwarding of remote server
*
* @param Net_SSH2 $ssh
* @return Boolean
* @access private
*/
function _request_forwarding($ssh)
{
$request_channel = $ssh->_get_open_channel();
if ($request_channel === false) {
return false;
}
$packet = pack('CNNa*C',
NET_SSH2_MSG_CHANNEL_REQUEST, $ssh->server_channels[$request_channel], strlen('auth-agent-req@openssh.com'), 'auth-agent-req@openssh.com', 1);
$ssh->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_REQUEST;
if (!$ssh->_send_binary_packet($packet)) {
return false;
}
$response = $ssh->_get_channel_packet($request_channel);
if ($response === false) {
return false;
}
$ssh->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_OPEN;
$this->forward_status = self::FORWARD_ACTIVE;
return true;
}
/**
* On successful channel open
*
* This method is called upon successful channel
* open to give the SSH Agent an opportunity
* to take further action. i.e. request agent forwarding
*
* @param Net_SSH2 $ssh
* @access private
*/
function _on_channel_open($ssh)
{
if ($this->forward_status == self::FORWARD_REQUEST) {
$this->_request_forwarding($ssh);
}
}
/**
* Forward data to SSH Agent and return data reply
*
* @param String $data
* @return data from SSH Agent
* @access private
*/
function _forward_data($data)
{
if ($this->expected_bytes > 0) {
$this->socket_buffer.= $data;
$this->expected_bytes -= strlen($data);
} else {
$agent_data_bytes = current(unpack('N', $data));
$current_data_bytes = strlen($data);
$this->socket_buffer = $data;
if ($current_data_bytes != $agent_data_bytes + 4) {
$this->expected_bytes = ($agent_data_bytes + 4) - $current_data_bytes;
return false;
}
}
if (strlen($this->socket_buffer) != fwrite($this->fsock, $this->socket_buffer)) {
user_error('Connection closed attempting to forward data to SSH agent');
}
$this->socket_buffer = '';
$this->expected_bytes = 0;
$agent_reply_bytes = current(unpack('N', fread($this->fsock, 4)));
$agent_reply_data = fread($this->fsock, $agent_reply_bytes);
$agent_reply_data = current(unpack('a*', $agent_reply_data));
return pack('Na*', $agent_reply_bytes, $agent_reply_data);
}
} }

View File

@ -30,5 +30,26 @@ class Functional_Net_SSH2AgentTest extends PhpseclibFunctionalTestCase
$ssh->login($this->getEnv('SSH_USERNAME'), $agent), $ssh->login($this->getEnv('SSH_USERNAME'), $agent),
'SSH2 login using Agent failed.' 'SSH2 login using Agent failed.'
); );
return array('ssh' => $ssh, 'ssh-agent' => $agent);
}
/**
* @depends testAgentLogin
*/
public function testAgentForward($args)
{
$ssh = $args['ssh'];
$agent = $args['ssh-agent'];
$hostname = $this->getEnv('SSH_HOSTNAME');
$username = $this->getEnv('SSH_USERNAME');
$this->assertEquals($username, trim($ssh->exec('whoami')));
$agent->startSSHForwarding($ssh);
$this->assertEquals($username, trim($ssh->exec("ssh " . $username . "@" . $hostname . ' \'whoami\'')));
return $args;
} }
} }

View File

@ -28,4 +28,6 @@ ssh-add "$HOME/.ssh/id_rsa"
# Allow the private key of the travis user to log in as phpseclib user # Allow the private key of the travis user to log in as phpseclib user
sudo mkdir -p "/home/$USERNAME/.ssh/" sudo mkdir -p "/home/$USERNAME/.ssh/"
sudo cp "$HOME/.ssh/id_rsa.pub" "/home/$USERNAME/.ssh/authorized_keys" sudo cp "$HOME/.ssh/id_rsa.pub" "/home/$USERNAME/.ssh/authorized_keys"
sudo ssh-keyscan -t rsa localhost > "/tmp/known_hosts"
sudo cp "/tmp/known_hosts" "/home/$USERNAME/.ssh/known_hosts"
sudo chown "$USERNAME:$USERNAME" "/home/$USERNAME/.ssh/" -R sudo chown "$USERNAME:$USERNAME" "/home/$USERNAME/.ssh/" -R