-
Notifications
You must be signed in to change notification settings - Fork 12
Add support for curve25519-sha256 key exchange method #306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
36b1036
Add support for curve25519-sha256 key exchange method
scott-xu 5f45fe5
Fix ClientSettingsTests.Defaults test
scott-xu 2beb511
Add KeyExchange base class
scott-xu 24d142b
Update README
scott-xu 510a795
Update per review
scott-xu 0d2f050
Update code style
scott-xu 17ceb74
Change sharedSecret from BigInteger to byte array
scott-xu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -566,6 +566,8 @@ Supported server key algorithms: | |
| - rsa-sha2-256 | ||
|
|
||
| Supported key exchange methods: | ||
| - curve25519-sha256 | ||
| - [email protected] | ||
| - ecdh-sha2-nistp256 | ||
| - ecdh-sha2-nistp384 | ||
| - ecdh-sha2-nistp521 | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,10 @@ static class AlgorithmNames // TODO: rename to KnownNames | |
| public static Name EcdhSha2Nistp384 => new Name(EcdhSha2Nistp384Bytes); | ||
| private static readonly byte[] EcdhSha2Nistp521Bytes = "ecdh-sha2-nistp521"u8.ToArray(); | ||
| public static Name EcdhSha2Nistp521 => new Name(EcdhSha2Nistp521Bytes); | ||
| private static readonly byte[] Curve25519Sha256Bytes = "curve25519-sha256"u8.ToArray(); | ||
| public static Name Curve25519Sha256 => new Name(Curve25519Sha256Bytes); | ||
| private static readonly byte[] Curve25519Sha256LibSshBytes = "[email protected]"u8.ToArray(); | ||
| public static Name Curve25519Sha256LibSsh => new Name(Curve25519Sha256LibSshBytes); | ||
|
|
||
| // Host key algorithms: key types and signature algorithms. | ||
| private static readonly byte[] SshRsaBytes = "ssh-rsa"u8.ToArray(); | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| // This file is part of Tmds.Ssh which is released under MIT. | ||
| // See file LICENSE for full license details. | ||
|
|
||
| using System.Buffers; | ||
| using System.Security.Cryptography; | ||
| using System.Numerics; | ||
| using Microsoft.Extensions.Logging; | ||
| using Org.BouncyCastle.Crypto.Generators; | ||
| using Org.BouncyCastle.Crypto.Parameters; | ||
| using Org.BouncyCastle.Security; | ||
| using Org.BouncyCastle.Crypto.Prng; | ||
| using Org.BouncyCastle.Crypto; | ||
| using Org.BouncyCastle.Crypto.Agreement; | ||
|
|
||
| namespace Tmds.Ssh; | ||
|
|
||
| // Curve25519 Key Exchange: https://datatracker.ietf.org/doc/html/rfc8731 | ||
| class Curve25519KeyExchange : KeyExchange, IKeyExchangeAlgorithm | ||
| { | ||
| public Curve25519KeyExchange() | ||
| :base(HashAlgorithmName.SHA256) | ||
| { } | ||
|
|
||
| public async Task<KeyExchangeOutput> TryExchangeAsync(KeyExchangeContext context, IHostKeyVerification hostKeyVerification, Packet firstPacket, KeyExchangeInput input, ILogger logger, CancellationToken ct) | ||
| { | ||
| var sequencePool = context.SequencePool; | ||
| var connectionInfo = input.ConnectionInfo; | ||
|
|
||
| AsymmetricCipherKeyPair x25519KeyPair; | ||
| using (var _randomGenerator = new CryptoApiRandomGenerator()) | ||
scott-xu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| var x25519KeyPairGenerator = new X25519KeyPairGenerator(); | ||
| x25519KeyPairGenerator.Init(new X25519KeyGenerationParameters(new SecureRandom(_randomGenerator))); | ||
| x25519KeyPair = x25519KeyPairGenerator.GenerateKeyPair(); | ||
| } | ||
|
|
||
| // Send ECDH_INIT. | ||
| var q_c = ((X25519PublicKeyParameters)x25519KeyPair.Public).GetEncoded(); | ||
scott-xu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| await context.SendPacketAsync(CreateEcdhInitMessage(sequencePool, q_c), ct).ConfigureAwait(false); | ||
|
|
||
| // Receive ECDH_REPLY. | ||
| using Packet ecdhReplyMsg = await context.ReceivePacketAsync(MessageId.SSH_MSG_KEX_ECDH_REPLY, firstPacket.Move(), ct).ConfigureAwait(false); | ||
| var ecdhReply = ParceEcdhReply(ecdhReplyMsg); | ||
|
|
||
| // Verify received key is valid. | ||
| var publicHostKey = await VerifyHostKeyAsync(hostKeyVerification, input, ecdhReply.public_host_key, ct).ConfigureAwait(false); | ||
scott-xu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Compute shared secret. | ||
| BigInteger sharedSecret; | ||
| try | ||
| { | ||
| sharedSecret = DeriveSharedSecret(x25519KeyPair.Private, new X25519PublicKeyParameters(ecdhReply.q_s)); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| throw new ConnectFailedException(ConnectFailedReason.KeyExchangeFailed, "Cannot determine shared secret.", connectionInfo, ex); | ||
| } | ||
|
|
||
| // Generate exchange hash. | ||
| byte[] exchangeHash = CalculateExchangeHash(sequencePool, input.ConnectionInfo, input.ClientKexInitMsg, input.ServerKexInitMsg, ecdhReply.public_host_key.Data, q_c, ecdhReply.q_s, sharedSecret); | ||
|
|
||
| // Verify the server's signature. | ||
| VerifySignature(publicHostKey, input.HostKeyAlgorithms, exchangeHash, ecdhReply.exchange_hash_signature, connectionInfo); | ||
|
|
||
| return CalculateKeyExchangeOutput(input, sequencePool, sharedSecret, exchangeHash); | ||
| } | ||
|
|
||
| private byte[] CalculateExchangeHash(SequencePool sequencePool, SshConnectionInfo connectionInfo, ReadOnlyPacket clientKexInitMsg, ReadOnlyPacket serverKexInitMsg, byte[] public_host_key, byte[] q_c, byte[] 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 BigInteger DeriveSharedSecret(AsymmetricKeyParameter privateKey, AsymmetricKeyParameter peerPublicKey) | ||
| { | ||
| var keyAgreement = new X25519Agreement(); | ||
| keyAgreement.Init(privateKey); | ||
|
|
||
| var rawSecretAgreement = new byte[keyAgreement.AgreementSize]; | ||
| keyAgreement.CalculateAgreement(peerPublicKey, rawSecretAgreement); | ||
| var sharedSecret = rawSecretAgreement.ToBigInteger(); | ||
| rawSecretAgreement.AsSpan().Clear(); | ||
| return sharedSecret; | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { } | ||
|
|
||
| private static Packet CreateEcdhInitMessage(SequencePool sequencePool, ReadOnlySpan<byte> q_c) | ||
| { | ||
| using var packet = sequencePool.RentPacket(); | ||
| var writer = packet.GetWriter(); | ||
| writer.WriteMessageId(MessageId.SSH_MSG_KEX_ECDH_INIT); | ||
| writer.WriteString(q_c); | ||
| return packet.Move(); | ||
| } | ||
|
|
||
| private static ( | ||
| SshKey public_host_key, | ||
| byte[] q_s, | ||
| ReadOnlySequence<byte> exchange_hash_signature) | ||
| ParceEcdhReply(ReadOnlyPacket packet) | ||
| { | ||
| var reader = packet.GetReader(); | ||
| reader.ReadMessageId(MessageId.SSH_MSG_KEX_ECDH_REPLY); | ||
| SshKey public_host_key = reader.ReadSshKey(); | ||
| byte[] q_s = reader.ReadStringAsByteArray(); | ||
| ReadOnlySequence<byte> exchange_hash_signature = reader.ReadStringAsBytes(); | ||
| reader.ReadEnd(); | ||
| return ( | ||
| public_host_key, | ||
| q_s, | ||
| exchange_hash_signature); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| using System; | ||
| using System.Buffers; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Numerics; | ||
| using System.Security.Cryptography; | ||
| using System.Text; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace Tmds.Ssh; | ||
|
|
||
| abstract class KeyExchange | ||
scott-xu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| protected readonly HashAlgorithmName _hashAlgorithmName; | ||
scott-xu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| public KeyExchange(HashAlgorithmName hashAlgorithmName) | ||
| { | ||
| this._hashAlgorithmName = hashAlgorithmName; | ||
| } | ||
|
|
||
| public static async Task<PublicKey> VerifyHostKeyAsync(IHostKeyVerification hostKeyVerification, KeyExchangeInput input, SshKey public_host_key, CancellationToken ct) | ||
scott-xu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| var connectionInfo = input.ConnectionInfo; | ||
| connectionInfo.ServerKey = new HostKey(public_host_key); | ||
| await hostKeyVerification.VerifyAsync(connectionInfo, ct).ConfigureAwait(false); | ||
|
|
||
| var publicHostKey = PublicKey.CreateFromSshKey(public_host_key); | ||
| if (publicHostKey is RsaPublicKey rsaPublicKey && rsaPublicKey.KeySize < input.MinimumRSAKeySize) | ||
| { | ||
| throw new ConnectFailedException(ConnectFailedReason.KeyExchangeFailed, $"Server RSA key size {rsaPublicKey.KeySize} is less than {input.MinimumRSAKeySize}.", connectionInfo); | ||
| } | ||
|
|
||
| return publicHostKey; | ||
| } | ||
|
|
||
| public static void VerifySignature(PublicKey publicHostKey, IReadOnlyList<Name> allowedAlgorithms, byte[] exchangeHash, ReadOnlySequence<byte> exchange_hash_signature, SshConnectionInfo connectionInfo) | ||
| { | ||
| if (!publicHostKey.VerifySignature(allowedAlgorithms, exchangeHash, exchange_hash_signature)) | ||
| { | ||
| throw new ConnectFailedException(ConnectFailedReason.KeyExchangeFailed, "Signature does not match host key.", connectionInfo); | ||
| } | ||
| } | ||
|
|
||
| public KeyExchangeOutput CalculateKeyExchangeOutput(KeyExchangeInput input, SequencePool sequencePool, BigInteger sharedSecret, byte[] exchangeHash) | ||
scott-xu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| byte[] sessionId = input.ConnectionInfo.SessionId ?? exchangeHash; | ||
| byte[] initialIVC2S = CalculateKey(sequencePool, sharedSecret, exchangeHash, (byte)'A', sessionId, input.InitialIVC2SLength); | ||
| byte[] initialIVS2C = CalculateKey(sequencePool, sharedSecret, exchangeHash, (byte)'B', sessionId, input.InitialIVS2CLength); | ||
| byte[] encryptionKeyC2S = CalculateKey(sequencePool, sharedSecret, exchangeHash, (byte)'C', sessionId, input.EncryptionKeyC2SLength); | ||
| byte[] encryptionKeyS2C = CalculateKey(sequencePool, sharedSecret, exchangeHash, (byte)'D', sessionId, input.EncryptionKeyS2CLength); | ||
| byte[] integrityKeyC2S = CalculateKey(sequencePool, sharedSecret, exchangeHash, (byte)'E', sessionId, input.IntegrityKeyC2SLength); | ||
| byte[] integrityKeyS2C = CalculateKey(sequencePool, sharedSecret, exchangeHash, (byte)'F', sessionId, input.IntegrityKeyS2CLength); | ||
|
|
||
| return new KeyExchangeOutput(exchangeHash, | ||
| initialIVS2C, encryptionKeyS2C, integrityKeyS2C, | ||
| initialIVC2S, encryptionKeyC2S, integrityKeyC2S); | ||
| } | ||
|
|
||
| private byte[] CalculateKey(SequencePool sequencePool, BigInteger sharedSecret, byte[] exchangeHash, byte c, byte[] sessionId, int keyLength) | ||
| { | ||
| // https://tools.ietf.org/html/rfc4253#section-7.2 | ||
|
|
||
| byte[] key = new byte[keyLength]; | ||
| int keyOffset = 0; | ||
|
|
||
| // HASH(K || H || c || session_id) | ||
| using Sequence sequence = sequencePool.RentSequence(); | ||
| var writer = new SequenceWriter(sequence); | ||
| writer.WriteMPInt(sharedSecret); | ||
| writer.Write(exchangeHash); | ||
| writer.WriteByte(c); | ||
| writer.Write(sessionId); | ||
|
|
||
| using IncrementalHash hash = IncrementalHash.CreateHash(_hashAlgorithmName); | ||
| foreach (var segment in sequence.AsReadOnlySequence()) | ||
| { | ||
| hash.AppendData(segment.Span); | ||
| } | ||
| byte[] K1 = hash.GetHashAndReset(); | ||
| Append(key, K1, ref keyOffset); | ||
|
|
||
| while (keyOffset != key.Length) | ||
| { | ||
| sequence.Clear(); | ||
|
|
||
| // K3 = HASH(K || H || K1 || K2) | ||
| writer = new SequenceWriter(sequence); | ||
| writer.WriteMPInt(sharedSecret); | ||
| writer.Write(exchangeHash); | ||
| writer.Write(key.AsSpan(0, keyOffset)); | ||
|
|
||
| foreach (var segment in sequence.AsReadOnlySequence()) | ||
| { | ||
| hash.AppendData(segment.Span); | ||
| } | ||
| byte[] Kn = hash.GetHashAndReset(); | ||
|
|
||
| Append(key, Kn, ref keyOffset); | ||
| } | ||
|
|
||
| return key; | ||
|
|
||
| static void Append(byte[] key, byte[] append, ref int offset) | ||
| { | ||
| int available = Math.Min(append.Length, key.Length - offset); | ||
| append.AsSpan().Slice(0, available).CopyTo(key.AsSpan(offset)); | ||
| offset += available; | ||
| } | ||
| } | ||
|
|
||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.