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 * @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 */
32
33namespace phpseclib3\System\SSH;
34
35use phpseclib3\Common\Functions\Strings;
36use phpseclib3\Crypt\PublicKeyLoader;
37use phpseclib3\Crypt\RSA;
38use phpseclib3\Exception\BadConfigurationException;
39use phpseclib3\Net\SSH2;
40use phpseclib3\System\SSH\Agent\Identity;
41
42/**
43 * Pure-PHP ssh-agent client identity factory
44 *
45 * requestIdentities() method pumps out \phpseclib3\System\SSH\Agent\Identity objects
46 *
47 * @author  Jim Wigginton <terrafrost@php.net>
48 */
49class Agent
50{
51    use Common\Traits\ReadBytes;
52
53    // Message numbers
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    // Agent forwarding status
65
66    // no forwarding requested and not active
67    const FORWARD_NONE = 0;
68    // request agent forwarding when opportune
69    const FORWARD_REQUEST = 1;
70    // forwarding has been request and is active
71    const FORWARD_ACTIVE = 2;
72
73    /**
74     * Unused
75     */
76    const SSH_AGENT_FAILURE = 5;
77
78    /**
79     * Socket Resource
80     *
81     * @var resource
82     */
83    private $fsock;
84
85    /**
86     * Agent forwarding status
87     *
88     * @var int
89     */
90    private $forward_status = self::FORWARD_NONE;
91
92    /**
93     * Buffer for accumulating forwarded authentication
94     * agent data arriving on SSH data channel destined
95     * for agent unix socket
96     *
97     * @var string
98     */
99    private $socket_buffer = '';
100
101    /**
102     * Tracking the number of bytes we are expecting
103     * to arrive for the agent socket on the SSH data
104     * channel
105     *
106     * @var int
107     */
108    private $expected_bytes = 0;
109
110    /**
111     * Default Constructor
112     *
113     * @return \phpseclib3\System\SSH\Agent
114     * @throws \phpseclib3\Exception\BadConfigurationException if SSH_AUTH_SOCK cannot be found
115     * @throws \RuntimeException on connection errors
116     */
117    public function __construct($address = null)
118    {
119        if (!$address) {
120            switch (true) {
121                case isset($_SERVER['SSH_AUTH_SOCK']):
122                    $address = $_SERVER['SSH_AUTH_SOCK'];
123                    break;
124                case isset($_ENV['SSH_AUTH_SOCK']):
125                    $address = $_ENV['SSH_AUTH_SOCK'];
126                    break;
127                default:
128                    throw new BadConfigurationException('SSH_AUTH_SOCK not found');
129            }
130        }
131
132        if (in_array('unix', stream_get_transports())) {
133            $this->fsock = fsockopen('unix://' . $address, 0, $errno, $errstr);
134            if (!$this->fsock) {
135                throw new \RuntimeException("Unable to connect to ssh-agent (Error $errno: $errstr)");
136            }
137        } else {
138            if (substr($address, 0, 9) != '\\\\.\\pipe\\' || strpos(substr($address, 9), '\\') !== false) {
139                throw new \RuntimeException('Address is not formatted as a named pipe should be');
140            }
141
142            $this->fsock = fopen($address, 'r+b');
143            if (!$this->fsock) {
144                throw new \RuntimeException('Unable to open address');
145            }
146        }
147    }
148
149    /**
150     * Request Identities
151     *
152     * See "2.5.2 Requesting a list of protocol 2 keys"
153     * Returns an array containing zero or more \phpseclib3\System\SSH\Agent\Identity objects
154     *
155     * @return array
156     * @throws \RuntimeException on receipt of unexpected packets
157     */
158    public function requestIdentities()
159    {
160        if (!$this->fsock) {
161            return [];
162        }
163
164        $packet = pack('NC', 1, self::SSH_AGENTC_REQUEST_IDENTITIES);
165        if (strlen($packet) != fputs($this->fsock, $packet)) {
166            throw new \RuntimeException('Connection closed while requesting identities');
167        }
168
169        $length = current(unpack('N', $this->readBytes(4)));
170        $packet = $this->readBytes($length);
171
172        list($type, $keyCount) = Strings::unpackSSH2('CN', $packet);
173        if ($type != self::SSH_AGENT_IDENTITIES_ANSWER) {
174            throw new \RuntimeException('Unable to request identities');
175        }
176
177        $identities = [];
178        for ($i = 0; $i < $keyCount; $i++) {
179            list($key_blob, $comment) = Strings::unpackSSH2('ss', $packet);
180            $temp = $key_blob;
181            list($key_type) = Strings::unpackSSH2('s', $temp);
182            switch ($key_type) {
183                case 'ssh-rsa':
184                case 'ssh-dss':
185                case 'ssh-ed25519':
186                case 'ecdsa-sha2-nistp256':
187                case 'ecdsa-sha2-nistp384':
188                case 'ecdsa-sha2-nistp521':
189                    $key = PublicKeyLoader::load($key_type . ' ' . base64_encode($key_blob));
190            }
191            // resources are passed by reference by default
192            if (isset($key)) {
193                $identity = (new Identity($this->fsock))
194                    ->withPublicKey($key)
195                    ->withPublicKeyBlob($key_blob);
196                $identities[] = $identity;
197                unset($key);
198            }
199        }
200
201        return $identities;
202    }
203
204    /**
205     * Signal that agent forwarding should
206     * be requested when a channel is opened
207     *
208     * @return void
209     */
210    public function startSSHForwarding()
211    {
212        if ($this->forward_status == self::FORWARD_NONE) {
213            $this->forward_status = self::FORWARD_REQUEST;
214        }
215    }
216
217    /**
218     * Request agent forwarding of remote server
219     *
220     * @param \phpseclib3\Net\SSH2 $ssh
221     * @return bool
222     */
223    private function request_forwarding(SSH2 $ssh)
224    {
225        if (!$ssh->requestAgentForwarding()) {
226            return false;
227        }
228
229        $this->forward_status = self::FORWARD_ACTIVE;
230
231        return true;
232    }
233
234    /**
235     * On successful channel open
236     *
237     * This method is called upon successful channel
238     * open to give the SSH Agent an opportunity
239     * to take further action. i.e. request agent forwarding
240     *
241     * @param \phpseclib3\Net\SSH2 $ssh
242     */
243    public function registerChannelOpen(SSH2 $ssh)
244    {
245        if ($this->forward_status == self::FORWARD_REQUEST) {
246            $this->request_forwarding($ssh);
247        }
248    }
249
250    /**
251     * Forward data to SSH Agent and return data reply
252     *
253     * @param string $data
254     * @return string Data from SSH Agent
255     * @throws \RuntimeException on connection errors
256     */
257    public function forwardData($data)
258    {
259        if ($this->expected_bytes > 0) {
260            $this->socket_buffer .= $data;
261            $this->expected_bytes -= strlen($data);
262        } else {
263            $agent_data_bytes = current(unpack('N', $data));
264            $current_data_bytes = strlen($data);
265            $this->socket_buffer = $data;
266            if ($current_data_bytes != $agent_data_bytes + 4) {
267                $this->expected_bytes = ($agent_data_bytes + 4) - $current_data_bytes;
268                return false;
269            }
270        }
271
272        if (strlen($this->socket_buffer) != fwrite($this->fsock, $this->socket_buffer)) {
273            throw new \RuntimeException('Connection closed attempting to forward data to SSH agent');
274        }
275
276        $this->socket_buffer = '';
277        $this->expected_bytes = 0;
278
279        $agent_reply_bytes = current(unpack('N', $this->readBytes(4)));
280
281        $agent_reply_data = $this->readBytes($agent_reply_bytes);
282        $agent_reply_data = current(unpack('a*', $agent_reply_data));
283
284        return pack('Na*', $agent_reply_bytes, $agent_reply_data);
285    }
286}
287