public function authenticate(\fpoirotte\Pssht\Messages\USERAUTH\REQUEST\Base $message, \fpoirotte\Pssht\Transport $transport, array &$context) { if (!$message instanceof \fpoirotte\Pssht\Messages\USERAUTH\REQUEST\PublicKey) { throw new \InvalidArgumentException(); } if ($message->getSignature() === null) { return self::AUTH_REJECT; } $logging = \Plop\Plop::getInstance(); $reverse = gethostbyaddr($transport->getAddress()); $algos = \fpoirotte\Pssht\Algorithms::factory(); $cls = $algos->getClass('PublicKey', $message->getAlgorithm()); if ($cls === null || !$this->store->exists($message->getUserName(), $message->getKey())) { $logging->info('Rejected public key connection from remote host "%(reverse)s" ' . 'to "%(luser)s" (unsupported key)', array('luser' => escape($message->getUserName()), 'reverse' => $reverse)); return self::AUTH_REJECT; } $key = $cls::loadPublic(base64_encode($message->getKey())); $encoder = new \fpoirotte\Pssht\Wire\Encoder(); $encoder->encodeString($context['DH']->getExchangeHash()); $encoder->encodeBytes(chr(\fpoirotte\Pssht\Messages\USERAUTH\REQUEST\Base::getMessageId())); $encoder->encodeString($message->getUserName()); $encoder->encodeString($message->getServiceName()); $encoder->encodeString(static::getName()); $encoder->encodeBoolean(true); $encoder->encodeString($message->getAlgorithm()); $encoder->encodeString($message->getKey()); if ($key->check($encoder->getBuffer()->get(0), $message->getSignature())) { $logging->info('Accepted public key connection from remote host "%(reverse)s" ' . 'to "%(luser)s" (using "%(algorithm)s" algorithm)', array('luser' => escape($message->getUserName()), 'reverse' => $reverse, 'algorithm' => escape($message->getAlgorithm()))); return self::AUTH_ACCEPT; } $logging->info('Rejected public key connection from remote host "%(reverse)s" ' . 'to "%(luser)s" (invalid signature)', array('luser' => escape($message->getUserName()), 'reverse' => $reverse)); return self::AUTH_REJECT; }
public function authenticate(\fpoirotte\Pssht\Messages\USERAUTH\REQUEST\Base $message, \fpoirotte\Pssht\Transport $transport, array &$context) { if (!$message instanceof \fpoirotte\Pssht\Messages\USERAUTH\REQUEST\HostBased) { throw new \InvalidArgumentException(); } $logging = \Plop\Plop::getInstance(); $reverse = gethostbyaddr($transport->getAddress()); $untrustedHost = rtrim($message->getHostname(), '.'); $algos = \fpoirotte\Pssht\Algorithms::factory(); $cls = $algos->getClass('PublicKey', $message->getAlgorithm()); if ($cls === null || !$this->store->exists($message->getUserName(), $message->getKey())) { $logging->info('Rejected host based connection from %(ruser)s@%(rhost)s ' . '(%(ruser)s@%(reverse)s) to "%(luser)s" ' . '(unsupported key)', array('ruser' => escape($message->getRemoteUser()), 'luser' => escape($message->getUserName()), 'rhost' => escape($untrustedHost), 'reverse' => $reverse)); return self::AUTH_REMOVE; } $key = $cls::loadPublic(base64_encode($message->getKey())); $encoder = new \fpoirotte\Pssht\Wire\Encoder(); $encoder->encodeString($context['DH']->getExchangeHash()); $encoder->encodeBytes(chr(\fpoirotte\Pssht\Messages\USERAUTH\REQUEST\Base::getMessageId())); $encoder->encodeString($message->getUserName()); $encoder->encodeString($message->getServiceName()); $encoder->encodeString(static::getName()); $encoder->encodeString($message->getAlgorithm()); $encoder->encodeString($message->getKey()); $encoder->encodeString($message->getHostname()); $encoder->encodeString($message->getRemoteUser()); if (!$key->check($encoder->getBuffer()->get(0), $message->getSignature())) { $logging->warn('Rejected host based connection from %(ruser)s@%(rhost)s ' . '(%(ruser)s@%(reverse)s) to "%(luser)s" (invalid signature)', array('ruser' => escape($message->getRemoteUser()), 'luser' => escape($message->getUserName()), 'rhost' => escape($untrustedHost), 'reverse' => $reverse)); return self::AUTH_REJECT; } if ($reverse !== $untrustedHost) { $logging->warning('Ignored reverse lookup mismatch for %(address)s (' . '"%(reverse)s" vs. "%(untrusted)s")', array('address' => $transport->getAddress(), 'reverse' => $reverse, 'untrusted' => escape($untrustedHost))); } if ($message->getUserName() !== $message->getRemoteUser()) { $logging->warning('Rejected host based connection from %(ruser)s@%(rhost)s ' . '(%(ruser)s@%(reverse)s): remote user does not match ' . 'local user (%(luser)s)', array('ruser' => escape($message->getRemoteUser()), 'luser' => escape($message->getUserName()), 'rhost' => escape($untrustedHost), 'reverse' => $reverse)); return self::AUTH_REMOVE; } $logging->info('Accepted host based connection ' . 'from "%(ruser)s@%(rhost)s" (%(ruser)s@%(reverse)s) ' . 'to "%(luser)s" (using "%(algorithm)s" algorithm)', array('ruser' => escape($message->getRemoteUser()), 'luser' => escape($message->getUserName()), 'rhost' => escape($untrustedHost), 'reverse' => $reverse, 'algorithm' => escape($message->getAlgorithm()))); return self::AUTH_ACCEPT; }
public function handleKEXINIT(\fpoirotte\Pssht\Transport $transport, array &$context) { $algos = \fpoirotte\Pssht\Algorithms::factory(); // Cookie $random = new \fpoirotte\Pssht\Random\OpenSSL(); // KEX $kexAlgos = $algos->getAlgorithms('KEX'); if (!count($kexAlgos)) { throw new \RuntimeException(); } // Server key $serverHostKeyAlgos = array_intersect($algos->getAlgorithms('PublicKey'), array_keys($context['serverKeys'])); if (!count($serverHostKeyAlgos)) { throw new \RuntimeException(); } // Encryption $encAlgosC2S = array_diff($algos->getAlgorithms('Encryption'), array('none')); $encAlgosS2C = $encAlgosC2S; if (!count($encAlgosC2S)) { throw new \RuntimeException(); } // MAC $macAlgosC2S = array_diff($algos->getAlgorithms('MAC'), array('none')); $macAlgosS2C = $macAlgosC2S; if (!count($macAlgosC2S)) { throw new \RuntimeException(); } // Compression $compAlgosC2S = $algos->getAlgorithms('Compression'); $compAlgosS2C = $compAlgosC2S; if (!count($compAlgosC2S)) { throw new \RuntimeException(); } $kex = new \fpoirotte\Pssht\Messages\KEXINIT($random, $kexAlgos, $serverHostKeyAlgos, $encAlgosC2S, $encAlgosS2C, $macAlgosC2S, $macAlgosS2C, $compAlgosC2S, $compAlgosS2C); $context['kex']['server'] = $kex; $transport->writeMessage($kex); return true; }
/** * Load the keys in the given file as if they belonged * to the specified user. * * \param string $user * User the keys belong to. * * \param string $file * File containing the keys to load. * It should follow the format of OpenSSH's * authorized_keys file. * * \retval File * Returns this loader. */ public function load($user, $file) { if (!is_string($user)) { throw new \InvalidArgumentException(); } if (!is_string($file)) { throw new \InvalidArgumentException(); } $algos = \fpoirotte\Pssht\Algorithms::factory(); $types = array('ssh-dss', 'ssh-rsa'); foreach (file($file) as $line) { $fields = explode(' ', preg_replace('/\\s+/', ' ', trim($line))); $max = count($fields); for ($i = 0; $i < $max; $i++) { if (in_array($fields[$i], $types, true)) { $cls = $algos->getClass('PublicKey', $fields[$i]); $this->store->add($user, $cls::loadPublic($fields[$i + 1])); break; } } } return $this; }
/** * Construct a new SSH transport layer. * * \param array $serverKeys * Keys presented by the server as an associated array where: * - keys indicate the key's algorithm (eg. "ssh-dss") * - values are an associative array with the following keys: * - "file": a PEM-encoded private key or path to a PEM-encoded * private key, in "file:///path/to/key.pem" format * - "passphrase": (optional) passphrase for the key * * \param fpoirotte::Pssht::Handlers::SERVICE::REQUEST $authMethods * Allowed authentication methods. * * \param fpoirotte::Pssht::Wire::Encoder $encoder * (optional) Encoder to use when sending SSH messages. * If omitted, a new encoder is automatically created. * * \param fpoirotte::Pssht::Wire::Decoder $decoder * (optional) Decoder to use when sending SSH messages. * If omitted, a new decoder is automatically created. * * \note * Once this class' constructor has been called, * you are advised to call the setAddress() method * to register the client's IP address. * This is required for some authentication methods * to work properly. */ public function __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) { if ($encoder === null) { $encoder = new \fpoirotte\Pssht\Wire\Encoder(); } if ($decoder === null) { $decoder = new \fpoirotte\Pssht\Wire\Decoder(); } if (!is_int($rekeyingBytes) || $rekeyingBytes <= 1024) { throw new \InvalidArgumentException(); } if (!is_int($rekeyingTime) || $rekeyingTime <= 60) { throw new \InvalidArgumentException(); } $algos = \fpoirotte\Pssht\Algorithms::factory(); $keys = array(); foreach ($serverKeys as $keyType => $params) { $cls = $algos->getClass('PublicKey', $keyType); if ($cls === null) { throw new \InvalidArgumentException(); } $passphrase = ''; if (isset($params['passphrase'])) { $passphrase = $params['passphrase']; } $keys[$keyType] = $cls::loadPrivate($params['file'], $passphrase); } $this->address = null; $this->appFactory = null; $this->banner = null; $this->context = array('rekeyingBytes' => 0, 'rekeyingTime' => time() + $rekeyingTime); $this->rekeyingBytes = $rekeyingBytes; $this->rekeyingTime = $rekeyingTime; $this->inSeqNo = 0; $this->outSeqNo = 0; $this->encoder = $encoder; $this->decoder = $decoder; $this->compressor = new \fpoirotte\Pssht\Compression\None(\fpoirotte\Pssht\CompressionInterface::MODE_COMPRESS); $this->uncompressor = new \fpoirotte\Pssht\Compression\None(\fpoirotte\Pssht\CompressionInterface::MODE_UNCOMPRESS); $this->encryptor = new \fpoirotte\Pssht\Encryption\None(null, null); $this->decryptor = new \fpoirotte\Pssht\Encryption\None(null, null); $this->inMAC = new \fpoirotte\Pssht\MAC\None(null); $this->outMAC = new \fpoirotte\Pssht\MAC\None(null); $this->handlers = array(\fpoirotte\Pssht\Messages\DISCONNECT::getMessageId() => new \fpoirotte\Pssht\Handlers\DISCONNECT(), \fpoirotte\Pssht\Messages\IGNORE::getMessageId() => new \fpoirotte\Pssht\Handlers\IGNORE(), \fpoirotte\Pssht\Messages\DEBUG::getMessageId() => new \fpoirotte\Pssht\Handlers\DEBUG(), \fpoirotte\Pssht\Messages\SERVICE\REQUEST::getMessageId() => $authMethods, \fpoirotte\Pssht\Messages\KEXINIT::getMessageId() => new \fpoirotte\Pssht\Handlers\KEXINIT(), \fpoirotte\Pssht\Messages\NEWKEYS::getMessageId() => new \fpoirotte\Pssht\Handlers\NEWKEYS(), 256 => new \fpoirotte\Pssht\Handlers\InitialState()); $ident = "SSH-2.0-pssht_1.0.x_dev"; $this->context['identity']['server'] = $ident; $this->context['serverKeys'] = $keys; $this->encoder->encodeBytes($ident . "\r\n"); }
public function handle($msgType, \fpoirotte\Pssht\Wire\Decoder $decoder, \fpoirotte\Pssht\Transport $transport, array &$context) { $algos = \fpoirotte\Pssht\Algorithms::factory(); $kex = \fpoirotte\Pssht\Messages\KEXINIT::unserialize($decoder); $context['kex']['client'] = $kex; if (!isset($context['rekeying'])) { $context['rekeying'] = 'client'; } // KEX method $context['kexAlgo'] = null; foreach ($kex->getKEXAlgos() as $algo) { if ($algos->getClass('KEX', $algo) !== null) { $kexCls = $context['kexAlgo'] = $algos->getClass('KEX', $algo); break; } } // No suitable KEX algorithm found. if (!isset($context['kexAlgo'])) { throw new \RuntimeException(); } $kexCls::addHandlers($transport); // C2S encryption $context['C2S']['Encryption'] = null; foreach ($kex->getC2SEncryptionAlgos() as $algo) { if ($algos->getClass('Encryption', $algo) !== null) { $context['C2S']['Encryption'] = $algos->getClass('Encryption', $algo); break; } } // No suitable C2S encryption cipher found. if (!isset($context['C2S']['Encryption'])) { throw new \RuntimeException(); } // C2S compression $context['C2S']['Compression'] = null; foreach ($kex->getC2SCompressionAlgos() as $algo) { if ($algos->getClass('Compression', $algo) !== null) { $context['C2S']['Compression'] = $algos->getClass('Compression', $algo); break; } } // No suitable C2S compression found. if (!isset($context['C2S']['Compression'])) { throw new \RuntimeException(); } // C2S MAC $context['C2S']['MAC'] = null; $reflector = new \ReflectionClass($context['C2S']['Encryption']); // Skip MAC algorithm selection for AEAD. if ($reflector->implementsInterface('\\fpoirotte\\Pssht\\AEADInterface')) { $context['C2S']['MAC'] = '\\fpoirotte\\Pssht\\MAC\\None'; } else { foreach ($kex->getC2SMACAlgos() as $algo) { if ($algos->getClass('MAC', $algo) !== null) { $context['C2S']['MAC'] = $algos->getClass('MAC', $algo); break; } } } // No suitable C2S MAC found. if (!isset($context['C2S']['MAC'])) { throw new \RuntimeException(); } // S2C encryption $context['S2C']['Encryption'] = null; foreach ($kex->getS2CEncryptionAlgos() as $algo) { if ($algos->getClass('Encryption', $algo) !== null) { $context['S2C']['Encryption'] = $algos->getClass('Encryption', $algo); break; } } // No suitable S2C encryption cipher found. if (!isset($context['S2C']['Encryption'])) { throw new \RuntimeException(); } // S2C compression $context['S2C']['Compression'] = null; foreach ($kex->getS2CCompressionAlgos() as $algo) { if ($algos->getClass('Compression', $algo) !== null) { $context['S2C']['Compression'] = $algos->getClass('Compression', $algo); break; } } // No suitable S2C compression found. if (!isset($context['S2C']['Compression'])) { throw new \RuntimeException(); } // S2C MAC $context['S2C']['MAC'] = null; $reflector = new \ReflectionClass($context['S2C']['Encryption']); // Skip MAC algorithm selection for AEAD. if ($reflector->implementsInterface('\\fpoirotte\\Pssht\\AEADInterface')) { $context['S2C']['MAC'] = '\\fpoirotte\\Pssht\\MAC\\None'; } else { foreach ($kex->getS2CMACAlgos() as $algo) { if ($algos->getClass('MAC', $algo) !== null) { $context['S2C']['MAC'] = $algos->getClass('MAC', $algo); break; } } } // No suitable S2C MAC found. if (!isset($context['S2C']['MAC'])) { throw new \RuntimeException(); } if ($context['rekeying'] === 'client') { $kexinit = new \fpoirotte\Pssht\Handlers\InitialState(); return $kexinit->handleKEXINIT($transport, $context); } return true; }
/** * Construct a new SSH_MSG_KEXDH_REPLY message. * * \param fpoirotte::Pssht::ECC::Curve $curve * Elliptic curve in use. * * \param fpoirotte::Pssht::Messages::KEX::ECDH::INIT::RFC5656 $kexDHInit * Client's contribution to the Diffie-Hellman Key Exchange. * * \param fpoirotte::Pssht::PublicKeyInterface $key * Server's public key. * * \param fpoirotte::Pssht::EncryptionInterface $encryptionAlgo * Encryption algorithm in use. * * \param fpoirotte::Pssht::KEXInterface $kexAlgo * Key exchange algorithm to use. * * \param fpoirotte::Pssht::Messages::KEXINIT $serverKEX * Algorithms supported by the server. * * \param fpoirotte::Pssht::Messages::KEXINIT $clientKEX * Algorithms supported by the client. * * \param string $serverIdent * Server's identification string * * \param string $clientIdent * Client's identification string */ public function __construct(\fpoirotte\Pssht\ECC\Curve $curve, \fpoirotte\Pssht\Messages\KEX\ECDH\INIT\RFC5656 $kexDHInit, \fpoirotte\Pssht\PublicKeyInterface $key, \fpoirotte\Pssht\EncryptionInterface $encryptionAlgo, \fpoirotte\Pssht\KEXInterface $kexAlgo, \fpoirotte\Pssht\Messages\KEXINIT $serverKEX, \fpoirotte\Pssht\Messages\KEXINIT $clientKEX, $serverIdent, $clientIdent) { if (!is_string($serverIdent)) { throw new \InvalidArgumentException(); } if (!is_string($clientIdent)) { throw new \InvalidArgumentException(); } $len = strlen(gmp_strval($curve->getOrder(), 2)); $len = ceil($len / 8); $randBytes = openssl_random_pseudo_bytes($len); $d_S = gmp_mod(gmp_init(bin2hex($randBytes), 16), $curve->getModulus()); $this->Q_S = $curve->getGenerator()->multiply($curve, $d_S); $Q_C = $kexDHInit->getQ(); /// @FIXME this is not optimal... $algorithms = \fpoirotte\Pssht\Algorithms::factory(); $cls = $algorithms->getClass('PublicKey', 'ecdsa-sha2-' . $curve->getName()); $clientPK = new $cls($Q_C); if (!$clientPK->isValid()) { throw new \InvalidArgumentException(); } // EC Co-factor DH (sec1-v2, section 3.3.2). $P = $Q_C->multiply($curve, gmp_mul($curve->getCofactor(), $d_S)); if ($P->isIdentity($curve)) { throw new \InvalidArgumentException(); } $this->K = $P->x; $this->curve = $curve; $this->K_S = $key; $this->kexDHInit = $kexDHInit; $this->kexAlgo = $kexAlgo; $this->serverKEX = $serverKEX; $this->clientKEX = $clientKEX; $this->serverIdent = $serverIdent; $this->clientIdent = $clientIdent; $msgId = chr(\fpoirotte\Pssht\Messages\KEXINIT::getMessageId()); // $sub is used to create the structure for the hashing function. $sub = new \fpoirotte\Pssht\Wire\Encoder(new \fpoirotte\Pssht\Buffer()); $this->K_S->serialize($sub); $K_S = $sub->getBuffer()->get(0); $sub->encodeString($this->clientIdent); $sub->encodeString($this->serverIdent); // $sub2 is used to compute the value // of various fields inside the structure. $sub2 = new \fpoirotte\Pssht\Wire\Encoder(new \fpoirotte\Pssht\Buffer()); $sub2->encodeBytes($msgId); // Add message identifier. $this->clientKEX->serialize($sub2); $sub->encodeString($sub2->getBuffer()->get(0)); $sub2->encodeBytes($msgId); // Add message identifier. $this->serverKEX->serialize($sub2); $sub->encodeString($sub2->getBuffer()->get(0)); $sub->encodeString($K_S); $sub->encodeString($Q_C->serialize($curve)); $sub->encodeString($this->Q_S->serialize($curve)); $sub->encodeMpint($this->K); $logging = \Plop\Plop::getInstance(); $origData = $sub->getBuffer()->get(0); $data = wordwrap(bin2hex($origData), 4, ' ', true); $data = wordwrap($data, 32 + 7, PHP_EOL, true); $logging->debug("Signature payload:\r\n%s", array($data)); $this->H = $this->kexAlgo->hash($origData); }