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