private byte[] CalculateExchangeHash(SequencePool sequencePool, SshConnectionInfo connectionInfo, Packet clientKexInitMsg, Packet serverKexInitMsg, ReadOnlySequence <byte> public_host_key, ECPoint q_c, ECPoint q_s, BigInteger sharedSecret) { /* * string V_C, client's identification string (CR and LF excluded) * string V_S, server's identification string (CR and LF excluded) * string I_C, payload of the client's SSH_MSG_KEXINIT * string I_S, payload of the server's SSH_MSG_KEXINIT * string K_S, server's public host key * string Q_C, client's ephemeral public key octet string * string Q_S, server's ephemeral public key octet string * mpint K, shared secret */ using Sequence sequence = sequencePool.RentSequence(); var writer = new SequenceWriter(sequence); writer.WriteString(connectionInfo.ClientIdentificationString !); writer.WriteString(connectionInfo.ServerIdentificationString !); writer.WriteString(clientKexInitMsg.Payload); writer.WriteString(serverKexInitMsg.Payload); writer.WriteString(public_host_key); writer.WriteString(q_c); writer.WriteString(q_s); writer.WriteMPInt(sharedSecret); using IncrementalHash hash = IncrementalHash.CreateHash(_hashAlgorithmName); foreach (var segment in sequence.AsReadOnlySequence()) { hash.AppendData(segment.Span); } return(hash.GetHashAndReset()); }
private static async Task PerformDefaultExchange(SshConnection connection, SshConnectionInfo connectionInfo, ILogger logger, SshClientSettings settings, CancellationToken ct) { // Protocol Version Exchange: https://tools.ietf.org/html/rfc4253#section-4.2. // The maximum length of the string is 255 characters, including the Carriage Return and Line Feed. const int MaxLineLength = 255 - 2; // The server MAY send other lines of data before sending the version string. const int MaxLineReads = 20; AssemblyInformationalVersionAttribute?versionAttribute = typeof(SshClient).Assembly.GetCustomAttribute <AssemblyInformationalVersionAttribute>(); string version = versionAttribute?.InformationalVersion ?? "0.0"; version = version.Replace('-', '_'); string identificationString = $"SSH-2.0-TmdsSsh_{version}"; connectionInfo.ClientIdentificationString = identificationString; // Send our identification string. logger.LogInformation("Local version string {identificationString}", identificationString); await connection.WriteLineAsync(identificationString, ct); // Receive peer identification string. for (int i = 0; i < MaxLineReads; i++) { string line = await connection.ReceiveLineAsync(MaxLineLength, ct); if (line.StartsWith("SSH-", StringComparison.Ordinal)) { connectionInfo.ServerIdentificationString = line; if (line.StartsWith("SSH-2.0-", StringComparison.Ordinal)) { logger.LogInformation("Remote version string {identificationString}", connectionInfo.ServerIdentificationString); return; } ThrowHelper.ThrowProtocolUnsupportedVersion(line); } } ThrowHelper.ThrowProtocolNoVersionIdentificationString(); }
public KeyExchangeInput(IReadOnlyList <Name> hostKeyAlgorithms, Packet exchangeInitMsg, Packet clientKexInitMsg, Packet serverKexInitMsg, SshConnectionInfo connectionInfo, int initialIVC2SLength, int initialIVS2CLength, int encryptionKeyC2SLength, int encryptionKeyS2CLength, int integrityKeyC2SLength, int integrityKeyS2CLength) { HostKeyAlgorithms = hostKeyAlgorithms; ExchangeInitMsg = exchangeInitMsg; ClientKexInitMsg = clientKexInitMsg; ServerKexInitMsg = serverKexInitMsg; ConnectionInfo = connectionInfo; InitialIVC2SLength = initialIVC2SLength; InitialIVS2CLength = initialIVS2CLength; EncryptionKeyC2SLength = encryptionKeyC2SLength; EncryptionKeyS2CLength = encryptionKeyS2CLength; IntegrityKeyC2SLength = integrityKeyC2SLength; IntegrityKeyS2CLength = integrityKeyS2CLength; }
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 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 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; } } }
private async Task HandleConnectionAsync(SshConnection connection, SshConnectionInfo connectionInfo) { try { try { Task sendTask = SendLoopAsync(connection); AddConnectionUser(sendTask); AddConnectionUser(ReceiveLoopAsync(connection, connectionInfo)); // Wait for a task that runs as long as the connection. await sendTask.ContinueWith(_ => { /* Ignore Failed/Canceled */ }); } catch (Exception e) // Unexpected: the continuation doesn't throw. { Abort(e); } finally { Task[] connectionUsers; lock (_gate) { connectionUsers = _connectionUsers !.ToArray(); // Accept no new users. _connectionUsers = null; } // Wait for all connection users. await Task.WhenAll(connectionUsers); } } catch (Exception e) { Abort(e); // Unlikely, Abort will be called already. } finally { connection.Dispose(); } async void AddConnectionUser(Task task) { lock (_gate) { _connectionUsers !.Add(task); } try { await task; RemoveConnectionUser(task); } catch (Exception e) { RemoveConnectionUser(task); Abort(e); } void RemoveConnectionUser(Task task) { lock (_gate) { List <Task> connectionUsers = _connectionUsers !; if (connectionUsers != null) { connectionUsers.Remove(task); } } } } }
private async Task RunConnectionAsync(CancellationToken connectCt, TaskCompletionSource <bool> connectTcs) { SshConnection?connection = null; var connectionInfo = new SshConnectionInfo(); try { // Cancel when: // * DisposeAsync is called (_abortCts) // * CancellationToken parameter from ConnectAsync (connectCt) // * Timeout from ConnectionSettings (ConnectTimeout) using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(connectCt, _abortCts.Token); connectCts.CancelAfter(_settings.ConnectTimeout); // Connect to the remote host connection = await _settings.EstablishConnectionAsync(_logger, _sequencePool, _settings, connectCts.Token); // Setup ssh connection if (!_settings.NoProtocolVersionExchange) { await _settings.ExchangeProtocolVersionAsync(connection, connectionInfo, _logger, _settings, connectCts.Token); } if (!_settings.NoKeyExchange) { using Packet localExchangeInitMsg = KeyExchange.CreateKeyExchangeInitMessage(_sequencePool, _logger, _settings); await connection.SendPacketAsync(localExchangeInitMsg, connectCts.Token); { using Packet remoteExchangeInitMsg = await connection.ReceivePacketAsync(connectCts.Token); if (remoteExchangeInitMsg.IsEmpty) { ThrowHelper.ThrowProtocolUnexpectedPeerClose(); } await _settings.ExchangeKeysAsync(connection, localExchangeInitMsg, remoteExchangeInitMsg, _logger, _settings, connectionInfo, connectCts.Token); } } if (!_settings.NoUserAuthentication) { await _settings.AuthenticateUserAsync(connection, _logger, _settings, connectionInfo, connectCts.Token); } // Allow sending. _sendQueue = Channel.CreateUnbounded <PendingSend>(new UnboundedChannelOptions { SingleWriter = false, SingleReader = true, AllowSynchronousContinuations = true }); // Allow connection users. _connectionUsers = new List <Task>(); // ConnectAsync completed successfully. connectTcs.SetResult(true); } catch (Exception e) { connection?.Dispose(); // In case the operation was canceled, change the exception based on the // token that triggered the cancellation. if (e is OperationCanceledException) { if (connectCt.IsCancellationRequested) { connectTcs.SetCanceled(); return; } else if (_abortCts.IsCancellationRequested) { e = NewObjectDisposedException(); } else { e = new TimeoutException(); } } // ConnectAsync failed. connectTcs.SetException(e); return; } await HandleConnectionAsync(connection, connectionInfo); }