public async Task <KeyExchangeOutput> TryExchangeAsync(SshConnection connection, KeyExchangeInput input, ILogger logger, CancellationToken ct) { var sequencePool = connection.SequencePool; using ECDiffieHellman ecdh = ECDiffieHellman.Create(_ecCurve); // Send ECDH_INIT. using ECDiffieHellmanPublicKey myPublicKey = ecdh.PublicKey; ECPoint q_c = myPublicKey.ExportParameters().Q; { using var ecdhInitMsg = CreateEcdhInitMessage(sequencePool, q_c); await connection.SendPacketAsync(ecdhInitMsg, ct); } // Receive ECDH_REPLY. Packet exchangeInitMsg = input.ExchangeInitMsg; using Packet exchangeInitMsgDispose = exchangeInitMsg.IsEmpty ? (exchangeInitMsg = await connection.ReceivePacketAsync(ct)) : default(Packet); var ecdhReply = ParceEcdhReply(exchangeInitMsg); // TODO: Verify received key is valid. // TODO: Verify host key belongs to server. // Compute shared secret. // TODO: what types of exceptions can we get when creating the public key? ECParameters parameters = new ECParameters { Curve = _ecCurve, Q = ecdhReply.q_s }; using ECDiffieHellman peerEcdh = ECDiffieHellman.Create(parameters); using ECDiffieHellmanPublicKey peerPublicKey = peerEcdh.PublicKey; BigInteger sharedSecret = DeriveSharedSecret(ecdh, peerPublicKey); var publicHostKey = PublicKey.Read(ecdhReply.public_host_key, input.HostKeyAlgorithms); // Generate exchange hash. byte[] exchangeHash = CalculateExchangeHash(sequencePool, input.ConnectionInfo, input.ClientKexInitMsg, input.ServerKexInitMsg, ecdhReply.public_host_key, q_c, ecdhReply.q_s, sharedSecret); // Verify the server's signature. if (!publicHostKey.VerifySignature(exchangeHash, ecdhReply.exchange_hash_signature)) { throw new KeyExchangeFailedException("Signature does not match host key."); } byte[] sessionId = input.ConnectionInfo.SessionId ?? exchangeHash; byte[] initialIVC2S = Hash(sequencePool, sharedSecret, exchangeHash, (byte)'A', sessionId, input.InitialIVC2SLength); byte[] initialIVS2C = Hash(sequencePool, sharedSecret, exchangeHash, (byte)'B', sessionId, input.InitialIVS2CLength); byte[] encryptionKeyC2S = Hash(sequencePool, sharedSecret, exchangeHash, (byte)'C', sessionId, input.EncryptionKeyC2SLength); byte[] encryptionKeyS2C = Hash(sequencePool, sharedSecret, exchangeHash, (byte)'D', sessionId, input.EncryptionKeyS2CLength); byte[] integrityKeyC2S = Hash(sequencePool, sharedSecret, exchangeHash, (byte)'E', sessionId, input.IntegrityKeyC2SLength); byte[] integrityKeyS2C = Hash(sequencePool, sharedSecret, exchangeHash, (byte)'F', sessionId, input.IntegrityKeyS2CLength); return(new KeyExchangeOutput(exchangeHash, initialIVS2C, encryptionKeyS2C, integrityKeyS2C, initialIVC2S, encryptionKeyC2S, integrityKeyC2S)); }
private async static Task PerformDefaultExchange(SshConnection connection, Packet clientKexInitMsg, Packet serverKexInitMsg, ILogger logger, SshClientSettings settings, SshConnectionInfo connectionInfo, CancellationToken ct) { // Key Exchange: https://tools.ietf.org/html/rfc4253#section-7. SequencePool sequencePool = connection.SequencePool; var remoteInit = ParseKeyExchangeInitMessage(serverKexInitMsg); // The chosen algorithm MUST be the first algorithm on the client's name-list // that is also on the server's name-list. Name encC2S = ChooseAlgorithm(settings.EncryptionAlgorithmsClientToServer, remoteInit.encryption_algorithms_client_to_server); Name encS2C = ChooseAlgorithm(settings.EncryptionAlgorithmsServerToClient, remoteInit.encryption_algorithms_server_to_client); Name macC2S = ChooseAlgorithm(settings.MacAlgorithmsClientToServer, remoteInit.mac_algorithms_client_to_server); Name macS2C = ChooseAlgorithm(settings.MacAlgorithmsServerToClient, remoteInit.mac_algorithms_server_to_client); Name comC2S = ChooseAlgorithm(settings.CompressionAlgorithmsClientToServer, remoteInit.compression_algorithms_client_to_server); Name comS2C = ChooseAlgorithm(settings.CompressionAlgorithmsServerToClient, remoteInit.compression_algorithms_server_to_client); if (encC2S.IsEmpty || encS2C.IsEmpty || macC2S.IsEmpty || macS2C.IsEmpty || comC2S.IsEmpty || comS2C.IsEmpty) { ThrowHelper.ThrowKeyExchangeFailed("No common encryption/integrity/compression algorithm."); } // Make an ordered list of host key algorithms. The key exchange algorithm will pick a compatible one. List <Name> hostKeyAlgorithms = new List <Name>(capacity: settings.ServerHostKeyAlgorithms.Count); foreach (var hostKeyAlgorithm in settings.ServerHostKeyAlgorithms) { if (remoteInit.server_host_key_algorithms.Contains(hostKeyAlgorithm)) { hostKeyAlgorithms.Add(hostKeyAlgorithm); } } // The first algorithm MUST be the preferred (and guessed) algorithm. If // both sides make the same guess, that algorithm MUST be used. Name matchingKex = settings.KeyExchangeAlgorithms.Count == 0 || remoteInit.kex_algorithms.Length == 0 || settings.KeyExchangeAlgorithms[0] != remoteInit.kex_algorithms[0] ? default(Name) : settings.KeyExchangeAlgorithms[0]; KeyExchangeOutput?keyExchangeOutput = null; Packet exchangeInitMsg = default; try { if (remoteInit.first_kex_packet_follows) { exchangeInitMsg = await connection.ReceivePacketAsync(ct); if (matchingKex.IsEmpty || settings.ServerHostKeyAlgorithms.Count == 0 || remoteInit.server_host_key_algorithms.Length == 0 || settings.ServerHostKeyAlgorithms[0] != remoteInit.server_host_key_algorithms[0]) { // Silently ignore if guessed wrong. exchangeInitMsg.Dispose(); exchangeInitMsg = default; } else { // Only accept the first hostKeyAlgorithm. if (hostKeyAlgorithms.Count > 1) { hostKeyAlgorithms.RemoveRange(1, hostKeyAlgorithms.Count - 1); } } } EncryptionFactory.Default.GetKeyAndIVLength(encC2S, out int encryptionKeyC2SLength, out int initialIVC2SLength); EncryptionFactory.Default.GetKeyAndIVLength(encS2C, out int encryptionKeyS2CLength, out int initialIVS2CLength); int integrityKeyC2SLength = HMacFactory.Default.GetKeyLength(macC2S); int integrityKeyS2CLength = HMacFactory.Default.GetKeyLength(macS2C); var keyExchangeInput = new KeyExchangeInput(hostKeyAlgorithms, exchangeInitMsg, clientKexInitMsg, serverKexInitMsg, connectionInfo, initialIVC2SLength, initialIVS2CLength, encryptionKeyC2SLength, encryptionKeyS2CLength, integrityKeyC2SLength, integrityKeyS2CLength); foreach (var keyAlgorithm in settings.KeyExchangeAlgorithms) { if (remoteInit.kex_algorithms.Contains(keyAlgorithm)) { logger.KeyExchangeAlgorithm(keyAlgorithm); using (var algorithm = KeyExchangeAlgorithmFactory.Default.Create(keyAlgorithm)) { keyExchangeOutput = await algorithm.TryExchangeAsync(connection, keyExchangeInput, logger, ct); } if (keyExchangeOutput != null) { connectionInfo.SessionId ??= keyExchangeOutput.ExchangeHash; break; } // Preferred algorithm must be used. if (!matchingKex.IsEmpty) { ThrowHelper.ThrowKeyExchangeFailed("Preferred key exchange algorithm failed."); } } } // If no algorithm satisfying all these conditions can be found, the // connection fails, and both sides MUST disconnect. if (keyExchangeOutput == null) { ThrowHelper.ThrowKeyExchangeFailed("Key exchange failed"); } } finally { exchangeInitMsg.Dispose(); } logger.AlgorithmsServerToClient(encS2C, macS2C, comS2C); logger.AlgorithmsClientToServer(encC2S, macC2S, comC2S); // Send SSH_MSG_NEWKEYS. { using Packet newKeysMsg = CreateNewKeysMessage(sequencePool); await connection.SendPacketAsync(newKeysMsg, ct); } // Receive SSH_MSG_NEWKEYS. using Packet newKeysReceivedMsg = await connection.ReceivePacketAsync(ct); ParseNewKeysMessage(newKeysReceivedMsg); var encrypt = EncryptionFactory.Default.CreateEncryptor(encC2S, keyExchangeOutput.EncryptionKeyC2S, keyExchangeOutput.InitialIVC2S); var macForEncoder = HMacFactory.Default.Create(macC2S, keyExchangeOutput.IntegrityKeyC2S); var decrypt = EncryptionFactory.Default.CreateDecryptor(encS2C, keyExchangeOutput.EncryptionKeyS2C, keyExchangeOutput.InitialIVS2C); var macForDecoder = HMacFactory.Default.Create(macS2C, keyExchangeOutput.IntegrityKeyS2C); connection.SetEncoderDecoder(new PacketEncoder(encrypt, macForEncoder), new PacketDecoder(sequencePool, decrypt, macForDecoder));
private async static Task PerformDefaultAuthentication(SshConnection connection, ILogger logger, SshClientSettings settings, SshConnectionInfo connectionInfo, CancellationToken ct) { // TODO: handle SSH_MSG_USERAUTH_BANNER. // Request ssh-userauth service { using var serviceRequestMsg = CreateServiceRequestMessage(connection.SequencePool); await connection.SendPacketAsync(serviceRequestMsg, ct); } { using Packet serviceAcceptMsg = await connection.ReceivePacketAsync(ct); ParseServiceAccept(serviceAcceptMsg); } // Try credentials. foreach (var credential in settings.Credentials) { if (credential is PasswordCredential passwordCredential) { logger.AuthenticationMethod("password"); using var userAuthMsg = CreatePasswordRequestMessage(connection.SequencePool, settings.UserName !, passwordCredential.Password); await connection.SendPacketAsync(userAuthMsg, ct); } else if (credential is IdentityFileCredential ifCredential) { if (IdentityFileCredential.TryParseFile(ifCredential.Filename, out PrivateKey? pk)) { using (pk) { logger.AuthenticationMethodPublicKey(ifCredential.Filename); using var userAuthMsg = CreatePublicKeyRequestMessage(connection.SequencePool, settings.UserName !, connectionInfo.SessionId !, pk !); await connection.SendPacketAsync(userAuthMsg, ct); } } else { continue; } } else { throw new NotImplementedException("Unsupported credential type: " + credential.GetType().FullName); } // TODO... using Packet response = await connection.ReceivePacketAsync(ct); if (IsAuthSuccesfull(response)) { logger.AuthenticationSucceeded(); return; } } throw new AuthenticationFailedException(); }
private async Task ReceiveLoopAsync(SshConnection connection, SshConnectionInfo connectionInfo) { CancellationToken abortToken = _abortCts.Token; while (true) { var packet = await connection.ReceivePacketAsync(abortToken, maxLength : Constants.MaxPacketLength); if (packet.IsEmpty) { Abort(ClosedByPeer); break; } else { // for now, eat everything. packet.Dispose(); } int msgType = packet.MessageType; // Connection Protocol: https://tools.ietf.org/html/rfc4254. // Dispatch to channels: // lock (_channels) // { // var channelExecution = _channels[channelNumber]; // channelExecution.QueueReceivedPacket(packet); // } // Handle global requests // ... switch (msgType) { case MessageNumber.SSH_MSG_KEXINIT: // Key Re-Exchange: https://tools.ietf.org/html/rfc4253#section-9. try { // When we send SSH_MSG_KEXINIT, we can't send other packets until key exchange completes. // This is implemented using _keyReExchangeSemaphore. var keyExchangeSemaphore = new SemaphoreSlim(0, 1); _keyReExchangeSemaphore = keyExchangeSemaphore; // this will await _keyReExchangeSemaphore and set it to null. using Packet clientKexInitMsg = KeyExchange.CreateKeyExchangeInitMessage(_sequencePool, _logger, _settings); try { await SendPacketAsync(clientKexInitMsg, abortToken); } catch { _keyReExchangeSemaphore.Dispose(); _keyReExchangeSemaphore = null; throw; } await _settings.ExchangeKeysAsync(connection, clientKexInitMsg, serverKexInitMsg : packet, _logger, _settings, connectionInfo, abortToken); keyExchangeSemaphore.Release(); } finally { packet.Dispose(); } break; } } }