pssht  latest
SSH server library written in PHP
Transport.php
1 <?php
2 
3 /*
4 * This file is part of pssht.
5 *
6 * (c) François Poirotte <clicky@erebot.net>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11 
12 namespace fpoirotte\Pssht;
13 
14 use \fpoirotte\Pssht\Compression\CompressionInterface;
15 
19 class Transport
20 {
22  protected $address;
23 
25  protected $inSeqNo;
26 
28  protected $outSeqNo;
29 
31  protected $encoder;
32 
34  protected $decoder;
35 
37  protected $encryptor;
38 
40  protected $decryptor;
41 
43  protected $compressor;
44 
46  protected $uncompressor;
47 
49  protected $inMAC;
50 
52  protected $outMAC;
53 
55  protected $context;
56 
58  protected $handlers;
59 
61  protected $appFactory;
62 
64  protected $banner;
65 
67  protected $rekeyingBytes;
68 
70  protected $rekeyingTime;
71 
73  protected $connected;
74 
75 
113  public function __construct(
114  array $serverKeys,
115  \fpoirotte\Pssht\Handlers\SERVICE\REQUEST $authMethods,
116  \fpoirotte\Pssht\Wire\Encoder $encoder = null,
117  \fpoirotte\Pssht\Wire\Decoder $decoder = null,
118  $rekeyingBytes = 1073741824,
119  $rekeyingTime = 3600
120  ) {
121  if ($encoder === null) {
122  $encoder = new \fpoirotte\Pssht\Wire\Encoder();
123  }
124 
125  if ($decoder === null) {
126  $decoder = new \fpoirotte\Pssht\Wire\Decoder();
127  }
128 
129  if (!is_int($rekeyingBytes) || $rekeyingBytes <= 1024) {
130  throw new \InvalidArgumentException();
131  }
132 
133  if (!is_int($rekeyingTime) || $rekeyingTime <= 60) {
134  throw new \InvalidArgumentException();
135  }
136 
138  $keys = array();
139  foreach ($serverKeys as $keyType => $params) {
140  $cls = $algos->getClass('Key', $keyType);
141  if ($cls === null) {
142  throw new \InvalidArgumentException();
143  }
144 
145  $passphrase = '';
146  if (isset($params['passphrase'])) {
147  $passphrase = $params['passphrase'];
148  }
149 
150  $keys[$keyType] = \fpoirotte\Pssht\KeyLoader\OpenSSH::loadPrivate(
151  file_get_contents($params['file']),
152  $passphrase
153  );
154  }
155 
156  $this->connected = false;
157  $this->address = null;
158  $this->appFactory = null;
159  $this->banner = null;
160  $this->context = array(
161  'rekeyingBytes' => 0,
162  'rekeyingTime' => time() + $rekeyingTime,
163  );
164 
165  $this->rekeyingBytes = $rekeyingBytes;
166  $this->rekeyingTime = $rekeyingTime;
167 
168  $this->inSeqNo = 0;
169  $this->outSeqNo = 0;
170 
171  $this->encoder = $encoder;
172  $this->decoder = $decoder;
173 
174  $this->compressor = new \fpoirotte\Pssht\Compression\None(CompressionInterface::MODE_COMPRESS);
175  $this->uncompressor = new \fpoirotte\Pssht\Compression\None(CompressionInterface::MODE_UNCOMPRESS);
176 
177  $this->encryptor = new \fpoirotte\Pssht\Encryption\None(null, null);
178  $this->decryptor = new \fpoirotte\Pssht\Encryption\None(null, null);
179 
180  $this->inMAC = new \fpoirotte\Pssht\MAC\None(null);
181  $this->outMAC = new \fpoirotte\Pssht\MAC\None(null);
182 
183  $this->handlers = array(
184  \fpoirotte\Pssht\Messages\DISCONNECT::getMessageId() =>
185  new \fpoirotte\Pssht\Handlers\DISCONNECT(),
186 
187  \fpoirotte\Pssht\Messages\IGNORE::getMessageId() =>
188  new \fpoirotte\Pssht\Handlers\IGNORE(),
189 
190  \fpoirotte\Pssht\Messages\DEBUG::getMessageId() =>
191  new \fpoirotte\Pssht\Handlers\DEBUG(),
192 
193  \fpoirotte\Pssht\Messages\SERVICE\REQUEST::getMessageId() =>
194  $authMethods,
195 
196  \fpoirotte\Pssht\Messages\KEXINIT::getMessageId() =>
197  new \fpoirotte\Pssht\Handlers\KEXINIT(),
198 
199  \fpoirotte\Pssht\Messages\NEWKEYS::getMessageId() =>
200  new \fpoirotte\Pssht\Handlers\NEWKEYS(),
201 
202  256 => new \fpoirotte\Pssht\Handlers\InitialState(),
203  );
204 
205  $ident = 'SSH-2.0-pssht_' .
206  str_replace(array(' ', '-'), '_', PSSHT_VERSION);
207  $this->context['identity']['server'] = $ident;
208  $this->context['serverKeys'] = $keys;
209  $this->encoder->encodeBytes($ident . "\r\n");
210  }
211 
229  public function setAddress($address)
230  {
231  if (!is_string($address)) {
232  throw new \InvalidArgumentException();
233  }
234 
235  if ($this->address !== null) {
236  throw new \RuntimeException();
237  }
238 
239  $this->address = $address;
240  $this->connected = true;
241  return $this;
242  }
243 
253  public function getAddress()
254  {
255  return $this->address;
256  }
257 
268  public function updateWriteStats($written)
269  {
270  if (!is_int($written)) {
271  throw new \InvalidArgumentException('Not an integer');
272  }
273  $time = time();
274  $this->context['rekeyingBytes'] += $written;
275 
276  if (isset($this->context['rekeying'])) {
277  // Do not restart key exchange
278  // if already rekeying.
279  return;
280  }
281 
282  $logging = \Plop\Plop::getInstance();
283  $stats = array(
284  'bytes' => $this->context['rekeyingBytes'],
285  'duration' =>
286  $time - $this->context['rekeyingTime'] +
287  $this->rekeyingTime,
288  );
289  $logging->debug(
290  '%(bytes)d bytes sent in %(duration)d seconds',
291  $stats
292  );
293 
294 
295  if ($this->context['rekeyingBytes'] >= $this->rekeyingBytes ||
296  $time >= $this->context['rekeyingTime']) {
297  $logging->debug('Initiating rekeying');
298  $this->context['rekeying'] = 'server';
299  $this->context['rekeyingBytes'] = 0;
300  $this->context['rekeyingTime'] = $time + $this->rekeyingTime;
301  $kexinit = new \fpoirotte\Pssht\Handlers\InitialState();
302  $kexinit->handleKEXINIT($this, $this->context);
303  }
304  }
305 
306  public function isConnected()
307  {
308  return $this->connected;
309  }
310 
317  public function getEncoder()
318  {
319  return $this->encoder;
320  }
321 
328  public function getDecoder()
329  {
330  return $this->decoder;
331  }
332 
339  public function getCompressor()
340  {
341  return $this->compressor;
342  }
343 
354  {
355  if ($compressor->getMode() !== CompressionInterface::MODE_COMPRESS) {
356  throw new \InvalidArgumentException();
357  }
358 
359  $this->compressor = $compressor;
360  return $this;
361  }
362 
369  public function getUncompressor()
370  {
371  return $this->uncompressor;
372  }
373 
384  {
385  if ($uncompressor->getMode() !== CompressionInterface::MODE_UNCOMPRESS) {
386  throw new \InvalidArgumentException();
387  }
388 
389  $this->uncompressor = $uncompressor;
390  return $this;
391  }
392 
399  public function getEncryptor()
400  {
401  return $this->encryptor;
402  }
403 
413  public function setEncryptor(\fpoirotte\Pssht\Encryption\EncryptionInterface $encryptor)
414  {
415  $this->encryptor = $encryptor;
416  return $this;
417  }
418 
425  public function getDecryptor()
426  {
427  return $this->decryptor;
428  }
429 
439  public function setDecryptor(\fpoirotte\Pssht\Encryption\EncryptionInterface $decryptor)
440  {
441  $this->decryptor = $decryptor;
442  return $this;
443  }
444 
451  public function getInputMAC()
452  {
453  return $this->inMAC;
454  }
455 
465  public function setInputMAC(\fpoirotte\Pssht\MAC\MACInterface $inputMAC)
466  {
467  $this->inMAC = $inputMAC;
468  return $this;
469  }
470 
477  public function getOutputMAC()
478  {
479  return $this->outMAC;
480  }
481 
491  public function setOutputMAC(\fpoirotte\Pssht\MAC\MACInterface $outputMAC)
492  {
493  $this->outMAC = $outputMAC;
494  return $this;
495  }
496 
503  public function getApplicationFactory()
504  {
505  return $this->applicationFactory;
506  }
507 
517  public function setApplicationFactory($factory)
518  {
519  $this->applicationFactory = $factory;
520  return $this;
521  }
522 
532  public function getBanner()
533  {
534  return $this->banner;
535  }
536 
546  public function setBanner($message)
547  {
548  if (!is_string($message)) {
549  throw new \InvalidArgumentException();
550  }
551 
552  $this->banner = $message;
553  return $this;
554  }
555 
569  public function getHandler($type)
570  {
571  if (!is_int($type) || $type < 0 || $type > 255) {
572  throw new \InvalidArgumentException();
573  }
574 
575  if (isset($this->handlers[$type])) {
576  return $this->handlers[$type];
577  }
578  return null;
579  }
580 
597  public function setHandler($type, \fpoirotte\Pssht\Handlers\HandlerInterface $handler)
598  {
599  if (!is_int($type) || $type < 0 || $type > 255) {
600  throw new \InvalidArgumentException();
601  }
602 
603  $this->handlers[$type] = $handler;
604  return $this;
605  }
606 
619  public function unsetHandler($type, \fpoirotte\Pssht\Handlers\HandlerInterface $handler)
620  {
621  if (!is_int($type) || $type < 0 || $type > 255) {
622  throw new \InvalidArgumentException();
623  }
624 
625  if (isset($this->handlers[$type]) && $this->handlers[$type] === $handler) {
626  unset($this->handlers[$type]);
627  }
628  return $this;
629  }
630 
640  public function writeMessage(\fpoirotte\Pssht\Messages\MessageInterface $message)
641  {
642  $logging = \Plop\Plop::getInstance();
643 
644  // Serialize the message.
645  $encoder = new \fpoirotte\Pssht\Wire\Encoder();
646  $encoder->encodeBytes(chr($message::getMessageId()));
647  $message->serialize($encoder);
648  $payload = $encoder->getBuffer()->get(0);
649  $logging->debug('Sending payload: %s', array(\escape($payload)));
650 
651  // Compress the payload if necessary.
652  $payload = $this->compressor->update($payload);
653  $size = strlen($payload);
654  $blockSize = max(8, $this->encryptor->getBlockSize());
655 
656  // Compute padding requirements.
657  // See http://api.libssh.org/rfc/PROTOCOL
658  // for more information on EtM (Encrypt-then-MAC)
659  // and RFCs 5116 & 5647 for AEAD & AES-GCM.
660  if ($this->outMAC instanceof \fpoirotte\Pssht\MAC\OpensshCom\EtM\EtMInterface) {
661  $padSize = $blockSize - ((1 + $size) % $blockSize);
662  } elseif ($this->encryptor instanceof \fpoirotte\Pssht\Algorithms\AEAD\AEADInterface) {
663  $padSize = $blockSize - ((1 + $size) % $blockSize);
664  } else {
665  $padSize = $blockSize - ((1 + 4 + $size) % $blockSize);
666  }
667  if ($padSize < 4) {
668  $padSize = ($padSize + $blockSize) % 256;
669  }
670  $padding = openssl_random_pseudo_bytes($padSize);
671 
672  // Create the packet. Every content passed to $encoder
673  // will be encrypted, except possibly for the packet
674  // length (see below).
675  $encoder->encodeUint32(1 + $size + $padSize);
676  if ($this->outMAC instanceof \fpoirotte\Pssht\MAC\OpensshCom\EtM\EtMInterface) {
677  // Send the packet length in plaintext.
678  $encSize = $encoder->getBuffer()->get(0);
679  $this->encoder->encodeBytes($encSize);
680  }
681  $encoder->encodeBytes(chr($padSize));
682  $encoder->encodeBytes($payload);
683  $encoder->encodeBytes($padding);
684  $packet = $encoder->getBuffer()->get(0);
685  $encrypted = $this->encryptor->encrypt($this->outSeqNo, $packet);
686 
687  // Compute the MAC.
688  if ($this->outMAC instanceof \fpoirotte\Pssht\MAC\OpensshCom\EtM\EtMInterface) {
689  $mac = $this->outMAC->compute($this->outSeqNo, $encSize . $encrypted);
690  } else {
691  $mac = $this->outMAC->compute($this->outSeqNo, $packet);
692  }
693 
694  // Send the packet on the wire.
695  $this->encoder->encodeBytes($encrypted);
696  $this->encoder->encodeBytes($mac);
697  $this->outSeqNo = ++$this->outSeqNo & 0xFFFFFFFF;
698 
699  $logging->debug(
700  'Sending %(type)s packet ' .
701  '(size: %(size)d, payload: %(payload)d, ' .
702  'block: %(block)d, padding: %(padding)d)',
703  array(
704  'type' => get_class($message),
705  'size' => strlen($encrypted),
706  'payload' => $size,
707  'block' => $blockSize,
708  'padding' => $padSize,
709  )
710  );
711 
712  if ($message instanceof \fpoirotte\Pssht\Messages\DISCONNECT) {
713  $this->connected = false;
714  }
715 
716  return $this;
717  }
718 
733  public function readMessage()
734  {
735  $logging = \Plop\Plop::getInstance();
736 
737  // Initial state: expect the client's identification string.
738  if (!isset($this->context['identity']['client'])) {
739  return $this->handlers[256]->handle(
740  null,
741  $this->decoder,
742  $this,
743  $this->context
744  );
745  }
746 
747  $blockSize = max($this->decryptor->getBlockSize(), 8);
748 
749  // See http://api.libssh.org/rfc/PROTOCOL
750  // for more information on EtM (Encrypt-then-MAC).
751  if ($this->inMAC instanceof \fpoirotte\Pssht\MAC\OpensshCom\EtM\EtMInterface) {
752  $encPayload = $this->decoder->getBuffer()->get(4);
753  if ($encPayload === null) {
754  return false;
755  }
756  $unencrypted = $encPayload;
757  } elseif ($this->decryptor instanceof \fpoirotte\Pssht\Algorithms\AEAD\AEADInterface) {
758  $encPayload = $this->decoder->getBuffer()->get(4);
759  if ($encPayload === null) {
760  return false;
761  }
762  $unencrypted = $this->decryptor->decrypt($this->inSeqNo, $encPayload);
763  $this->decoder->getBuffer()->unget($encPayload);
764  $encPayload = '';
765  } else {
766  $encPayload = $this->decoder->getBuffer()->get($blockSize);
767  if ($encPayload === null) {
768  return false;
769  }
770  $unencrypted = $this->decryptor->decrypt($this->inSeqNo, $encPayload);
771  }
772  $buffer = new \fpoirotte\Pssht\Buffer($unencrypted);
773  $decoder = new \fpoirotte\Pssht\Wire\Decoder($buffer);
774  $packetLength = $decoder->decodeUint32();
775 
776  // Read the rest of the message.
777  if ($this->inMAC instanceof \fpoirotte\Pssht\MAC\OpensshCom\EtM\EtMInterface) {
778  // Only the main payload remains.
779  $toRead = $packetLength;
780  } elseif ($this->decryptor instanceof \fpoirotte\Pssht\Algorithms\AEAD\AEADInterface) {
781  // packet length (authenticated data)
782  // + encrypted payload
783  // + authentication tag (AT)
784  $toRead = 4 + $packetLength + $this->decryptor->getSize();
785  } else {
786  $toRead =
787  // Note: we must account for the "packet length" field
788  // not being included in $packetLength itself and for
789  // the $blockSize bytes we have already read.
790  4 - $blockSize +
791 
792  // Rest of the encrypted data.
793  $packetLength;
794  }
795 
796  if ($toRead < 0) {
797  throw new \RuntimeException();
798  }
799 
800  $unencrypted2 = '';
801  if ($toRead !== 0) {
802  $encPayload2 = $this->decoder->getBuffer()->get($toRead);
803  if ($encPayload2 === null) {
804  $this->decoder->getBuffer()->unget($encPayload);
805  return false;
806  }
807  $unencrypted2 = $this->decryptor->decrypt($this->inSeqNo, $encPayload2);
808  if ($unencrypted2 === null) {
809  return false;
810  }
811  $buffer->push($unencrypted2);
812  }
813 
814  $paddingLength = ord($decoder->decodeBytes());
815  $payload = $decoder->decodeBytes($packetLength - $paddingLength - 1);
816  $padding = $decoder->decodeBytes($paddingLength);
817 
818  // If a MAC is in use.
819  $macSize = $this->inMAC->getSize();
820  $actualMAC = '';
821  if ($macSize > 0) {
822  $actualMAC = $this->decoder->getBuffer()->get($macSize);
823  if ($actualMAC === null) {
824  $this->decoder->getBuffer()->unget($encPayload2)->unget($encPayload);
825  return false;
826  }
827 
828  if ($this->inMAC instanceof \fpoirotte\Pssht\MAC\OpensshCom\EtM\EtMInterface) {
829  // $encPayload actually contains packet length (in plaintext).
830  $macData = $encPayload . $encPayload2;
831  } else {
832  $macData = $unencrypted . $unencrypted2;
833  }
834 
835  $expectedMAC = $this->inMAC->compute(
836  $this->inSeqNo,
837  ((string) substr($macData, 0, $packetLength + 4))
838  );
839 
840  if ($expectedMAC !== $actualMAC) {
841  throw new \RuntimeException();
842  }
843  }
844 
845  if (!isset($packetLength, $paddingLength, $payload, $padding, $actualMAC)) {
846  $this->decoder->getBuffer()->unget($actualMAC)->unget($encPayload2)->unget($encPayload);
847  $logging->error('Something went wrong during decoding');
848  return false;
849  }
850 
851  $payload = $this->uncompressor->update($payload);
852  $decoder = new \fpoirotte\Pssht\Wire\Decoder(new \fpoirotte\Pssht\Buffer($payload));
853  $msgType = ord($decoder->decodeBytes(1));
854  $logging->debug('Received payload: %s', array(\escape($payload)));
855 
856  $res = true;
857  if (isset($this->handlers[$msgType])) {
858  $handler = $this->handlers[$msgType];
859  $logging->debug(
860  'Calling %(handler)s with message type #%(msgType)d',
861  array(
862  'handler' => get_class($handler) . '::handle',
863  'msgType' => $msgType,
864  )
865  );
866  try {
867  $res = $handler->handle($msgType, $decoder, $this, $this->context);
868  } catch (\fpoirotte\Pssht\Messages\DISCONNECT $e) {
869  if ($e->getCode() !== 0) {
870  $this->writeMessage($e);
871  }
872  throw $e;
873  }
874  } else {
875  $logging->warn('Unimplemented message type (%d)', array($msgType));
876  $response = new \fpoirotte\Pssht\Messages\UNIMPLEMENTED($this->inSeqNo);
877  $this->writeMessage($response);
878  }
879 
880  $this->inSeqNo = ++$this->inSeqNo & 0xFFFFFFFF;
881  return $res;
882  }
883 }
setDecryptor(\fpoirotte\Pssht\Encryption\EncryptionInterface $decryptor)
Definition: Transport.php:439
const MODE_COMPRESS
Use the algorithm for compression.
setOutputMAC(\fpoirotte\Pssht\MAC\MACInterface $outputMAC)
Definition: Transport.php:491
$context
Context for this SSH connection.
Definition: Transport.php:55
setHandler($type,\fpoirotte\Pssht\Handlers\HandlerInterface $handler)
Definition: Transport.php:597
$handlers
Registered handlers for this SSH connection.
Definition: Transport.php:58
$rekeyingTime
Maximum duration before rekeying.
Definition: Transport.php:70
$compressor
Output compression.
Definition: Transport.php:43
$outSeqNo
Output sequence number.
Definition: Transport.php:28
$decryptor
Input cipher.
Definition: Transport.php:40
setApplicationFactory($factory)
Definition: Transport.php:517
$rekeyingBytes
Maximum number of bytes exchanged before rekeying.
Definition: Transport.php:67
$inSeqNo
Input sequence number.
Definition: Transport.php:25
setEncryptor(\fpoirotte\Pssht\Encryption\EncryptionInterface $encryptor)
Definition: Transport.php:413
setInputMAC(\fpoirotte\Pssht\MAC\MACInterface $inputMAC)
Definition: Transport.php:465
setCompressor(CompressionInterface $compressor)
Definition: Transport.php:353
setUncompressor(CompressionInterface $uncompressor)
Definition: Transport.php:383
const MODE_UNCOMPRESS
Use the algorithm for decompression.
$address
Address (ip:port) of the client.
Definition: Transport.php:22
$uncompressor
Input compression.
Definition: Transport.php:46
$connected
Whether this client is still connected or not.
Definition: Transport.php:73
$appFactory
Factory for the application.
Definition: Transport.php:61
__construct(array $serverKeys,\fpoirotte\Pssht\Handlers\SERVICE\REQUEST $authMethods,\fpoirotte\Pssht\Wire\Encoder $encoder=null,\fpoirotte\Pssht\Wire\Decoder $decoder=null, $rekeyingBytes=1073741824, $rekeyingTime=3600)
Definition: Transport.php:113
writeMessage(\fpoirotte\Pssht\Messages\MessageInterface $message)
Definition: Transport.php:640
unsetHandler($type,\fpoirotte\Pssht\Handlers\HandlerInterface $handler)
Definition: Transport.php:619
$encryptor
Output cipher.
Definition: Transport.php:37