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