1<?php
2
3/**
4 * Pure-PHP ssh-agent client.
5 *
6 * PHP version 5
7 *
8 * Here are some examples of how to use this library:
9 * <code>
10 * <?php
11 *    include 'vendor/autoload.php';
12 *
13 *    $agent = new \phpseclib\System\SSH\Agent();
14 *
15 *    $ssh = new \phpseclib\Net\SSH2('www.domain.tld');
16 *    if (!$ssh->login('username', $agent)) {
17 *        exit('Login Failed');
18 *    }
19 *
20 *    echo $ssh->exec('pwd');
21 *    echo $ssh->exec('ls -la');
22 * ?>
23 * </code>
24 *
25 * @category  System
26 * @package   SSH\Agent
27 * @author    Jim Wigginton <terrafrost@php.net>
28 * @copyright 2014 Jim Wigginton
29 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
30 * @link      http://phpseclib.sourceforge.net
31 * @internal  See http://api.libssh.org/rfc/PROTOCOL.agent
32 */
33
34namespace phpseclib\System\SSH;
35
36use phpseclib\Crypt\RSA;
37use phpseclib\System\SSH\Agent\Identity;
38
39/**
40 * Pure-PHP ssh-agent client identity factory
41 *
42 * requestIdentities() method pumps out \phpseclib\System\SSH\Agent\Identity objects
43 *
44 * @package SSH\Agent
45 * @author  Jim Wigginton <terrafrost@php.net>
46 * @access  public
47 */
48class Agent
49{
50    /**#@+
51     * Message numbers
52     *
53     * @access private
54     */
55    // to request SSH1 keys you have to use SSH_AGENTC_REQUEST_RSA_IDENTITIES (1)
56    const SSH_AGENTC_REQUEST_IDENTITIES = 11;
57    // this is the SSH2 response; the SSH1 response is SSH_AGENT_RSA_IDENTITIES_ANSWER (2).
58    const SSH_AGENT_IDENTITIES_ANSWER = 12;
59    // the SSH1 request is SSH_AGENTC_RSA_CHALLENGE (3)
60    const SSH_AGENTC_SIGN_REQUEST = 13;
61    // the SSH1 response is SSH_AGENT_RSA_RESPONSE (4)
62    const SSH_AGENT_SIGN_RESPONSE = 14;
63    /**#@-*/
64
65    /**@+
66     * Agent forwarding status
67     *
68     * @access private
69     */
70    // no forwarding requested and not active
71    const FORWARD_NONE = 0;
72    // request agent forwarding when opportune
73    const FORWARD_REQUEST = 1;
74    // forwarding has been request and is active
75    const FORWARD_ACTIVE = 2;
76    /**#@-*/
77
78    /**
79     * Unused
80     */
81    const SSH_AGENT_FAILURE = 5;
82
83    /**
84     * Socket Resource
85     *
86     * @var resource
87     * @access private
88     */
89    var $fsock;
90
91    /**
92     * Agent forwarding status
93     *
94     * @access private
95     */
96    var $forward_status = self::FORWARD_NONE;
97
98    /**
99     * Buffer for accumulating forwarded authentication
100     * agent data arriving on SSH data channel destined
101     * for agent unix socket
102     *
103     * @access private
104     */
105    var $socket_buffer = '';
106
107    /**
108     * Tracking the number of bytes we are expecting
109     * to arrive for the agent socket on the SSH data
110     * channel
111     */
112    var $expected_bytes = 0;
113
114    /**
115     * Default Constructor
116     *
117     * @return \phpseclib\System\SSH\Agent
118     * @access public
119     */
120    function __construct($address = null)
121    {
122        if (!$address) {
123            switch (true) {
124                case isset($_SERVER['SSH_AUTH_SOCK']):
125                    $address = $_SERVER['SSH_AUTH_SOCK'];
126                    break;
127                case isset($_ENV['SSH_AUTH_SOCK']):
128                    $address = $_ENV['SSH_AUTH_SOCK'];
129                    break;
130                default:
131                    user_error('SSH_AUTH_SOCK not found');
132                    return false;
133            }
134        }
135
136        $this->fsock = fsockopen('unix://' . $address, 0, $errno, $errstr);
137        if (!$this->fsock) {
138            user_error("Unable to connect to ssh-agent (Error $errno: $errstr)");
139        }
140    }
141
142    /**
143     * Request Identities
144     *
145     * See "2.5.2 Requesting a list of protocol 2 keys"
146     * Returns an array containing zero or more \phpseclib\System\SSH\Agent\Identity objects
147     *
148     * @return array
149     * @access public
150     */
151    function requestIdentities()
152    {
153        if (!$this->fsock) {
154            return array();
155        }
156
157        $packet = pack('NC', 1, self::SSH_AGENTC_REQUEST_IDENTITIES);
158        if (strlen($packet) != fputs($this->fsock, $packet)) {
159            user_error('Connection closed while requesting identities');
160            return array();
161        }
162
163        $temp = fread($this->fsock, 4);
164        if (strlen($temp) != 4) {
165            user_error('Connection closed while requesting identities');
166            return array();
167        }
168        $length = current(unpack('N', $temp));
169        $type = ord(fread($this->fsock, 1));
170        if ($type != self::SSH_AGENT_IDENTITIES_ANSWER) {
171            user_error('Unable to request identities');
172            return array();
173        }
174
175        $identities = array();
176        $temp = fread($this->fsock, 4);
177        if (strlen($temp) != 4) {
178            user_error('Connection closed while requesting identities');
179            return array();
180        }
181        $keyCount = current(unpack('N', $temp));
182        for ($i = 0; $i < $keyCount; $i++) {
183            $temp = fread($this->fsock, 4);
184            if (strlen($temp) != 4) {
185                user_error('Connection closed while requesting identities');
186                return array();
187            }
188            $length = current(unpack('N', $temp));
189            $key_blob = fread($this->fsock, $length);
190            if (strlen($key_blob) != $length) {
191                user_error('Connection closed while requesting identities');
192                return array();
193            }
194            $key_str = 'ssh-rsa ' . base64_encode($key_blob);
195            $temp = fread($this->fsock, 4);
196            if (strlen($temp) != 4) {
197                user_error('Connection closed while requesting identities');
198                return array();
199            }
200            $length = current(unpack('N', $temp));
201            if ($length) {
202                $temp = fread($this->fsock, $length);
203                if (strlen($temp) != $length) {
204                    user_error('Connection closed while requesting identities');
205                    return array();
206                }
207                $key_str.= ' ' . $temp;
208            }
209            $length = current(unpack('N', substr($key_blob, 0, 4)));
210            $key_type = substr($key_blob, 4, $length);
211            switch ($key_type) {
212                case 'ssh-rsa':
213                    $key = new RSA();
214                    $key->loadKey($key_str);
215                    break;
216                case 'ssh-dss':
217                    // not currently supported
218                    break;
219            }
220            // resources are passed by reference by default
221            if (isset($key)) {
222                $identity = new Identity($this->fsock);
223                $identity->setPublicKey($key);
224                $identity->setPublicKeyBlob($key_blob);
225                $identities[] = $identity;
226                unset($key);
227            }
228        }
229
230        return $identities;
231    }
232
233    /**
234     * Signal that agent forwarding should
235     * be requested when a channel is opened
236     *
237     * @param Net_SSH2 $ssh
238     * @return bool
239     * @access public
240     */
241    function startSSHForwarding($ssh)
242    {
243        if ($this->forward_status == self::FORWARD_NONE) {
244            $this->forward_status = self::FORWARD_REQUEST;
245        }
246    }
247
248    /**
249     * Request agent forwarding of remote server
250     *
251     * @param Net_SSH2 $ssh
252     * @return bool
253     * @access private
254     */
255    function _request_forwarding($ssh)
256    {
257        $request_channel = $ssh->_get_open_channel();
258        if ($request_channel === false) {
259            return false;
260        }
261
262        $packet = pack(
263            'CNNa*C',
264            NET_SSH2_MSG_CHANNEL_REQUEST,
265            $ssh->server_channels[$request_channel],
266            strlen('auth-agent-req@openssh.com'),
267            'auth-agent-req@openssh.com',
268            1
269        );
270
271        $ssh->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_REQUEST;
272
273        if (!$ssh->_send_binary_packet($packet)) {
274            return false;
275        }
276
277        $response = $ssh->_get_channel_packet($request_channel);
278        if ($response === false) {
279            return false;
280        }
281
282        $ssh->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_OPEN;
283        $this->forward_status = self::FORWARD_ACTIVE;
284
285        return true;
286    }
287
288    /**
289     * On successful channel open
290     *
291     * This method is called upon successful channel
292     * open to give the SSH Agent an opportunity
293     * to take further action. i.e. request agent forwarding
294     *
295     * @param Net_SSH2 $ssh
296     * @access private
297     */
298    function _on_channel_open($ssh)
299    {
300        if ($this->forward_status == self::FORWARD_REQUEST) {
301            $this->_request_forwarding($ssh);
302        }
303    }
304
305    /**
306     * Forward data to SSH Agent and return data reply
307     *
308     * @param string $data
309     * @return data from SSH Agent
310     * @access private
311     */
312    function _forward_data($data)
313    {
314        if ($this->expected_bytes > 0) {
315            $this->socket_buffer.= $data;
316            $this->expected_bytes -= strlen($data);
317        } else {
318            $agent_data_bytes = current(unpack('N', $data));
319            $current_data_bytes = strlen($data);
320            $this->socket_buffer = $data;
321            if ($current_data_bytes != $agent_data_bytes + 4) {
322                $this->expected_bytes = ($agent_data_bytes + 4) - $current_data_bytes;
323                return false;
324            }
325        }
326
327        if (strlen($this->socket_buffer) != fwrite($this->fsock, $this->socket_buffer)) {
328            user_error('Connection closed attempting to forward data to SSH agent');
329            return false;
330        }
331
332        $this->socket_buffer = '';
333        $this->expected_bytes = 0;
334
335        $temp = fread($this->fsock, 4);
336        if (strlen($temp) != 4) {
337            user_error('Connection closed while reading data response');
338            return false;
339        }
340        $agent_reply_bytes = current(unpack('N', $temp));
341
342        $agent_reply_data = fread($this->fsock, $agent_reply_bytes);
343        if (strlen($agent_reply_data) != $agent_reply_bytes) {
344            user_error('Connection closed while reading data response');
345            return false;
346        }
347        $agent_reply_data = current(unpack('a*', $agent_reply_data));
348
349        return pack('Na*', $agent_reply_bytes, $agent_reply_data);
350    }
351}
352