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