1<?php
2
3/**
4 * Pure-PHP ssh-agent client.
5 *
6 * {@internal See http://api.libssh.org/rfc/PROTOCOL.agent}
7 *
8 * PHP version 5
9 *
10 * Here are some examples of how to use this library:
11 * <code>
12 * <?php
13 *    include 'vendor/autoload.php';
14 *
15 *    $agent = new \phpseclib3\System\SSH\Agent();
16 *
17 *    $ssh = new \phpseclib3\Net\SSH2('www.domain.tld');
18 *    if (!$ssh->login('username', $agent)) {
19 *        exit('Login Failed');
20 *    }
21 *
22 *    echo $ssh->exec('pwd');
23 *    echo $ssh->exec('ls -la');
24 * ?>
25 * </code>
26 *
27 * @category  System
28 * @package   SSH\Agent
29 * @author    Jim Wigginton <terrafrost@php.net>
30 * @copyright 2014 Jim Wigginton
31 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
32 * @link      http://phpseclib.sourceforge.net
33 */
34
35namespace phpseclib3\System\SSH;
36
37use phpseclib3\Common\Functions\Strings;
38use phpseclib3\Crypt\PublicKeyLoader;
39use phpseclib3\Crypt\RSA;
40use phpseclib3\Exception\BadConfigurationException;
41use phpseclib3\System\SSH\Agent\Identity;
42
43/**
44 * Pure-PHP ssh-agent client identity factory
45 *
46 * requestIdentities() method pumps out \phpseclib3\System\SSH\Agent\Identity objects
47 *
48 * @package SSH\Agent
49 * @author  Jim Wigginton <terrafrost@php.net>
50 * @access  public
51 */
52class Agent
53{
54    use Common\Traits\ReadBytes;
55
56    // Message numbers
57
58    // to request SSH1 keys you have to use SSH_AGENTC_REQUEST_RSA_IDENTITIES (1)
59    const SSH_AGENTC_REQUEST_IDENTITIES = 11;
60    // this is the SSH2 response; the SSH1 response is SSH_AGENT_RSA_IDENTITIES_ANSWER (2).
61    const SSH_AGENT_IDENTITIES_ANSWER = 12;
62    // the SSH1 request is SSH_AGENTC_RSA_CHALLENGE (3)
63    const SSH_AGENTC_SIGN_REQUEST = 13;
64    // the SSH1 response is SSH_AGENT_RSA_RESPONSE (4)
65    const SSH_AGENT_SIGN_RESPONSE = 14;
66
67    // Agent forwarding status
68
69    // no forwarding requested and not active
70    const FORWARD_NONE = 0;
71    // request agent forwarding when opportune
72    const FORWARD_REQUEST = 1;
73    // forwarding has been request and is active
74    const FORWARD_ACTIVE = 2;
75
76    /**
77     * Unused
78     */
79    const SSH_AGENT_FAILURE = 5;
80
81    /**
82     * Socket Resource
83     *
84     * @var resource
85     * @access private
86     */
87    private $fsock;
88
89    /**
90     * Agent forwarding status
91     *
92     * @var int
93     * @access private
94     */
95    private $forward_status = self::FORWARD_NONE;
96
97    /**
98     * Buffer for accumulating forwarded authentication
99     * agent data arriving on SSH data channel destined
100     * for agent unix socket
101     *
102     * @var string
103     * @access private
104     */
105    private $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 int
113     * @access private
114     */
115    private $expected_bytes = 0;
116
117    /**
118     * The current request channel
119     *
120     * @var int
121     * @access private
122     */
123    private $request_channel;
124
125    /**
126     * Default Constructor
127     *
128     * @return \phpseclib3\System\SSH\Agent
129     * @throws \phpseclib3\Exception\BadConfigurationException if SSH_AUTH_SOCK cannot be found
130     * @throws \RuntimeException on connection errors
131     * @access public
132     */
133    public function __construct($address = null)
134    {
135        if (!$address) {
136            switch (true) {
137                case isset($_SERVER['SSH_AUTH_SOCK']):
138                    $address = $_SERVER['SSH_AUTH_SOCK'];
139                    break;
140                case isset($_ENV['SSH_AUTH_SOCK']):
141                    $address = $_ENV['SSH_AUTH_SOCK'];
142                    break;
143                default:
144                    throw new BadConfigurationException('SSH_AUTH_SOCK not found');
145            }
146        }
147
148        $this->fsock = fsockopen('unix://' . $address, 0, $errno, $errstr);
149        if (!$this->fsock) {
150            throw new \RuntimeException("Unable to connect to ssh-agent (Error $errno: $errstr)");
151        }
152    }
153
154    /**
155     * Request Identities
156     *
157     * See "2.5.2 Requesting a list of protocol 2 keys"
158     * Returns an array containing zero or more \phpseclib3\System\SSH\Agent\Identity objects
159     *
160     * @return array
161     * @throws \RuntimeException on receipt of unexpected packets
162     * @access public
163     */
164    public function requestIdentities()
165    {
166        if (!$this->fsock) {
167            return [];
168        }
169
170        $packet = pack('NC', 1, self::SSH_AGENTC_REQUEST_IDENTITIES);
171        if (strlen($packet) != fputs($this->fsock, $packet)) {
172            throw new \RuntimeException('Connection closed while requesting identities');
173        }
174
175        $length = current(unpack('N', $this->readBytes(4)));
176        $packet = $this->readBytes($length);
177
178        list($type, $keyCount) = Strings::unpackSSH2('CN', $packet);
179        if ($type != self::SSH_AGENT_IDENTITIES_ANSWER) {
180            throw new \RuntimeException('Unable to request identities');
181        }
182
183        $identities = [];
184        for ($i = 0; $i < $keyCount; $i++) {
185            list($key_blob, $comment) = Strings::unpackSSH2('ss', $packet);
186            $temp = $key_blob;
187            list($key_type) = Strings::unpackSSH2('s', $temp);
188            switch ($key_type) {
189                case 'ssh-rsa':
190                case 'ssh-dss':
191                case 'ssh-ed25519':
192                case 'ecdsa-sha2-nistp256':
193                case 'ecdsa-sha2-nistp384':
194                case 'ecdsa-sha2-nistp521':
195                    $key = PublicKeyLoader::load($key_type . ' ' . base64_encode($key_blob));
196            }
197            // resources are passed by reference by default
198            if (isset($key)) {
199                $identity = (new Identity($this->fsock))
200                    ->withPublicKey($key)
201                    ->withPublicKeyBlob($key_blob);
202                $identities[] = $identity;
203                unset($key);
204            }
205        }
206
207        return $identities;
208    }
209
210    /**
211     * Signal that agent forwarding should
212     * be requested when a channel is opened
213     *
214     * @return void
215     * @access public
216     */
217    public function startSSHForwarding()
218    {
219        if ($this->forward_status == self::FORWARD_NONE) {
220            $this->forward_status = self::FORWARD_REQUEST;
221        }
222    }
223
224    /**
225     * Request agent forwarding of remote server
226     *
227     * @param \phpseclib3\Net\SSH2 $ssh
228     * @return bool
229     * @access private
230     */
231    private function request_forwarding($ssh)
232    {
233        if (!$ssh->requestAgentForwarding()) {
234            return false;
235        }
236
237        $this->forward_status = self::FORWARD_ACTIVE;
238
239        return true;
240    }
241
242    /**
243     * On successful channel open
244     *
245     * This method is called upon successful channel
246     * open to give the SSH Agent an opportunity
247     * to take further action. i.e. request agent forwarding
248     *
249     * @param \phpseclib3\Net\SSH2 $ssh
250     * @access private
251     */
252    public function registerChannelOpen($ssh)
253    {
254        if ($this->forward_status == self::FORWARD_REQUEST) {
255            $this->request_forwarding($ssh);
256        }
257    }
258
259    /**
260     * Forward data to SSH Agent and return data reply
261     *
262     * @param string $data
263     * @return string Data from SSH Agent
264     * @throws \RuntimeException on connection errors
265     * @access public
266     */
267    public function forwardData($data)
268    {
269        if ($this->expected_bytes > 0) {
270            $this->socket_buffer .= $data;
271            $this->expected_bytes -= strlen($data);
272        } else {
273            $agent_data_bytes = current(unpack('N', $data));
274            $current_data_bytes = strlen($data);
275            $this->socket_buffer = $data;
276            if ($current_data_bytes != $agent_data_bytes + 4) {
277                $this->expected_bytes = ($agent_data_bytes + 4) - $current_data_bytes;
278                return false;
279            }
280        }
281
282        if (strlen($this->socket_buffer) != fwrite($this->fsock, $this->socket_buffer)) {
283            throw new \RuntimeException('Connection closed attempting to forward data to SSH agent');
284        }
285
286        $this->socket_buffer = '';
287        $this->expected_bytes = 0;
288
289        $agent_reply_bytes = current(unpack('N', $this->readBytes(4)));
290
291        $agent_reply_data = $this->readBytes($agent_reply_bytes);
292        $agent_reply_data = current(unpack('a*', $agent_reply_data));
293
294        return pack('Na*', $agent_reply_bytes, $agent_reply_data);
295    }
296}
297