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 
33 namespace phpseclib3\System\SSH;
34 
35 use phpseclib3\Common\Functions\Strings;
36 use phpseclib3\Crypt\Common\PublicKey;
37 use phpseclib3\Crypt\PublicKeyLoader;
38 use phpseclib3\Crypt\RSA;
39 use phpseclib3\Exception\BadConfigurationException;
40 use phpseclib3\Net\SSH2;
41 use 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  */
50 class 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