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