public static function loadPrivate($pem, $passphrase = '') { if (!is_string($pem)) { throw new \InvalidArgumentException(); } if (!is_string($passphrase)) { throw new \InvalidArgumentException(); } /// @FIXME support passphrase-protected EdDSA private keys. if ($passphrase !== '') { throw new \RuntimeException(); } if (strncmp($pem, 'file://', 7) === 0) { $key = file_get_contents(substr($pem, 7)); } else { $key = $pem; } $header = '-----BEGIN OPENSSH PRIVATE KEY-----'; $footer = '-----END OPENSSH PRIVATE KEY-----'; if (strncmp($key, $header, strlen($header)) !== 0) { throw new \InvalidArgumentException(); } elseif (substr($key, -strlen($footer)) !== $footer) { throw new \InvalidArgumentException(); } $key = base64_decode(substr($key, strlen($header), -strlen($footer))); if (strncmp($key, static::AUTH_MAGIC, strlen(static::AUTH_MAGIC))) { throw new \InvalidArgumentException(); } $decoder = new \fpoirotte\Pssht\Wire\Decoder(); $decoder->getBuffer()->push(substr($key, strlen(static::AUTH_MAGIC))); $ciphername = $decoder->decodeString(); /// @FIXME support encrypted private keys if ($ciphername !== 'none') { throw new \InvalidArgumentException(); } $kdfname = $decoder->decodeString(); $kdfoptions = $decoder->decodeString(); $numKeys = $decoder->decodeUint32(); $publicKey = array(); // Block malicious inputs if ($numKeys <= 0 || $numKeys >= 0x80000000) { throw new \InvalidArgumentException(); } for ($i = 0; $i < $numKeys; $i++) { $tmp = new \fpoirotte\Pssht\Wire\Decoder(); $tmp->getBuffer()->push($decoder->decodeString()); // Reject unknown key identifiers if ($tmp->decodeString() !== static::getName()) { continue; } $publicKey[$i] = $tmp->decodeString(); } $tmp = new \fpoirotte\Pssht\Wire\Decoder(); $tmp->getBuffer()->push($decoder->decodeString()); // Both "checkint" fields must have the same value. if ($tmp->decodeUint32() !== $tmp->decodeUint32()) { throw new \InvalidArgumentException(); } // Reject unknown identifiers. if ($tmp->decodeString() !== static::getName()) { throw new \InvalidArgumentException(); } // Discard public key blob (duplicate). $tmp->decodeString(); $secretKey = array(); for ($i = 0; $i < $numKeys; $i++) { $secretKey[$i] = $tmp->decodeString(); // Discard comment field. $tmp->decodeString(); } // Should we also ensure that a correct padding // has been applied? $pk = reset($publicKey); if (!isset($secretKey[key($publicKey)])) { throw new \InvalidArgumentException(); } $sk = $secretKey[key($publicKey)]; return new static($pk, $sk); }
/** * Try to read and handle a single SSH message. * * \retval bool * \b true if a message was successfully read and handled, * \b false otherwise. * * \note * Depending on the circumstances, messages may be successfully * read but left unhandled (eg. because the message was incomplete). * In such cases, the message will be reinjected and \b false * returned, making it possible for a future call to this method * to handle the (full) message again. */ public function readMessage() { $logging = \Plop\Plop::getInstance(); // Initial state: expect the client's identification string. if (!isset($this->context['identity']['client'])) { return $this->handlers[256]->handle(null, $this->decoder, $this, $this->context); } $blockSize = max($this->decryptor->getBlockSize(), 8); // See http://api.libssh.org/rfc/PROTOCOL // for more information on EtM (Encrypt-then-MAC). if ($this->inMAC instanceof \fpoirotte\Pssht\MAC\OpensshCom\EtM\EtMInterface) { $encPayload = $this->decoder->getBuffer()->get(4); if ($encPayload === null) { return false; } $unencrypted = $encPayload; } elseif ($this->decryptor instanceof \fpoirotte\Pssht\AEADInterface) { $encPayload = $this->decoder->getBuffer()->get(4); if ($encPayload === null) { return false; } $unencrypted = $this->decryptor->decrypt($this->inSeqNo, $encPayload); $this->decoder->getBuffer()->unget($encPayload); $encPayload = ''; } else { $encPayload = $this->decoder->getBuffer()->get($blockSize); if ($encPayload === null) { return false; } $unencrypted = $this->decryptor->decrypt($this->inSeqNo, $encPayload); } $buffer = new \fpoirotte\Pssht\Buffer($unencrypted); $decoder = new \fpoirotte\Pssht\Wire\Decoder($buffer); $packetLength = $decoder->decodeUint32(); // Read the rest of the message. if ($this->inMAC instanceof \fpoirotte\Pssht\MAC\OpensshCom\EtM\EtMInterface) { // Only the main payload remains. $toRead = $packetLength; } elseif ($this->decryptor instanceof \fpoirotte\Pssht\AEADInterface) { // packet length (authenticated data) // + encrypted payload // + authentication tag (AT) $toRead = 4 + $packetLength + $this->decryptor->getSize(); } else { $toRead = 4 - $blockSize + $packetLength; } if ($toRead < 0) { throw new \RuntimeException(); } $unencrypted2 = ''; if ($toRead !== 0) { $encPayload2 = $this->decoder->getBuffer()->get($toRead); if ($encPayload2 === null) { $this->decoder->getBuffer()->unget($encPayload); return false; } $unencrypted2 = $this->decryptor->decrypt($this->inSeqNo, $encPayload2); if ($unencrypted2 === null) { return false; } $buffer->push($unencrypted2); } $paddingLength = ord($decoder->decodeBytes()); $payload = $decoder->decodeBytes($packetLength - $paddingLength - 1); $padding = $decoder->decodeBytes($paddingLength); // If a MAC is in use. $macSize = $this->inMAC->getSize(); $actualMAC = ''; if ($macSize > 0) { $actualMAC = $this->decoder->getBuffer()->get($macSize); if ($actualMAC === null) { $this->decoder->getBuffer()->unget($encPayload2)->unget($encPayload); return false; } if ($this->inMAC instanceof \fpoirotte\Pssht\MAC\OpensshCom\EtM\EtMInterface) { // $encPayload actually contains packet length (in plaintext). $macData = $encPayload . $encPayload2; } else { $macData = $unencrypted . $unencrypted2; } $expectedMAC = $this->inMAC->compute($this->inSeqNo, (string) substr($macData, 0, $packetLength + 4)); if ($expectedMAC !== $actualMAC) { throw new \RuntimeException(); } } if (!isset($packetLength, $paddingLength, $payload, $padding, $actualMAC)) { $this->decoder->getBuffer()->unget($actualMAC)->unget($encPayload2)->unget($encPayload); $logging->error('Something went wrong during decoding'); return false; } $payload = $this->uncompressor->update($payload); $decoder = new \fpoirotte\Pssht\Wire\Decoder(new \fpoirotte\Pssht\Buffer($payload)); $msgType = ord($decoder->decodeBytes(1)); $logging->debug('Received payload: %s', array(\escape($payload))); $res = true; if (isset($this->handlers[$msgType])) { $handler = $this->handlers[$msgType]; $logging->debug('Calling %(handler)s with message type #%(msgType)d', array('handler' => get_class($handler) . '::handle', 'msgType' => $msgType)); try { $res = $handler->handle($msgType, $decoder, $this, $this->context); } catch (\fpoirotte\Pssht\Messages\DISCONNECT $e) { if ($e->getCode() !== 0) { $this->writeMessage($e); } throw $e; } } else { $logging->warn('Unimplemented message type (%d)', array($msgType)); $response = new \fpoirotte\Pssht\Messages\UNIMPLEMENTED($this->inSeqNo); $this->writeMessage($response); } $this->inSeqNo = ++$this->inSeqNo & 0xffffffff; return $res; }