Use BCL ECDiffieHellman for KeyExchange instead of BouncyCastle (.NET 8.0 onward only) (#1371)

* Use BCL ECDiffieHellman for KeyExchange (.NET 8.0 onward only)

* Add back an empty line

* Remove the BouncyCastle dependency when target .NET 8.0 onward.

* Run KeyExchangeAlgorithmTests for .NET 6.0

* Build Renci.SshNet.IntegrationTests.csproj for net6.0

* Update filter

* Add back BouncyCastle as fallback

* Add back the missing `SendMessage`

* Run ECDH KEX integration tests under .NET48

* Use SshNamedCurves instead of SecNamedCurves for BouncyCastle.
BCL supports both names. See https://github.com/dotnet/runtime/blob/main/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/OidLookup.cs#L200-L202

* typo

* Fix build

* Use System.Security.Cryptography namespace if NET8_0_OR_GREATER;
Use one parameter constructor for class ECDomainParameters

* Separate BCL and BouncyCastle implementation

---------

Co-authored-by: Wojciech Nagórski <wojtpl2@gmail.com>
This commit is contained in:
Scott Xu
2024-08-01 05:06:14 +08:00
committed by GitHub
parent 252c732c5e
commit 58c85ee79c
7 changed files with 225 additions and 38 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ for:
- sh: dotnet test -f net8.0 -c Debug --no-restore --no-build --results-directory artifacts --logger Appveyor --logger "console;verbosity=normal" --logger "liquid.md;LogFileName=linux_unit_test_net_8_report.md" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput=../../artifacts/linux_unit_test_net_8_coverage.xml test/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
- sh: echo "Run integration tests"
- sh: dotnet test -f net8.0 -c Debug --no-restore --no-build --results-directory artifacts --logger Appveyor --logger "console;verbosity=normal" --logger "liquid.md;LogFileName=linux_integration_test_net_8_report.md" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput=../../artifacts/linux_integration_test_net_8_coverage.xml test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj
- sh: dotnet test -f net48 -c Debug --no-restore --no-build --results-directory artifacts --logger Appveyor --logger "console;verbosity=normal" --logger "liquid.md;LogFileName=linux_integration_test_net_48_report.md" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput=../../artifacts/linux_integration_test_net_48_coverage.xml --filter "Name=ChaCha20Poly1305|Name~Zlib" test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj
- sh: dotnet test -f net48 -c Debug --no-restore --no-build --results-directory artifacts --logger Appveyor --logger "console;verbosity=normal" --logger "liquid.md;LogFileName=linux_integration_test_net_48_report.md" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput=../../artifacts/linux_integration_test_net_48_coverage.xml --filter "Name=ChaCha20Poly1305|Name~Ecdh|Name~Zlib" test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj
-
matrix:
@@ -0,0 +1,77 @@
#if NET8_0_OR_GREATER
using System;
using System.Security.Cryptography;
namespace Renci.SshNet.Security
{
internal abstract partial class KeyExchangeECDH
{
private sealed class BclImpl : Impl
{
private readonly ECCurve _curve;
private readonly ECDiffieHellman _clientECDH;
public BclImpl(ECCurve curve)
{
_curve = curve;
_clientECDH = ECDiffieHellman.Create();
}
public override byte[] GenerateClientECPoint()
{
_clientECDH.GenerateKey(_curve);
var q = _clientECDH.PublicKey.ExportParameters().Q;
return EncodeECPoint(q);
}
public override byte[] CalculateAgreement(byte[] serverECPoint)
{
var q = DecodeECPoint(serverECPoint);
var parameters = new ECParameters
{
Curve = _curve,
Q = q,
};
using var serverECDH = ECDiffieHellman.Create(parameters);
return _clientECDH.DeriveRawSecretAgreement(serverECDH.PublicKey);
}
private static byte[] EncodeECPoint(ECPoint point)
{
var q = new byte[1 + point.X.Length + point.Y.Length];
q[0] = 0x04;
Buffer.BlockCopy(point.X, 0, q, 1, point.X.Length);
Buffer.BlockCopy(point.Y, 0, q, point.X.Length + 1, point.Y.Length);
return q;
}
private static ECPoint DecodeECPoint(byte[] q)
{
var cordSize = (q.Length - 1) / 2;
var x = new byte[cordSize];
var y = new byte[cordSize];
Buffer.BlockCopy(q, 1, x, 0, x.Length);
Buffer.BlockCopy(q, cordSize + 1, y, 0, y.Length);
return new ECPoint { X = x, Y = y };
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_clientECDH.Dispose();
}
}
}
}
}
#endif
@@ -0,0 +1,44 @@
using Org.BouncyCastle.Asn1.X9;
using Org.BouncyCastle.Crypto.Agreement;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Renci.SshNet.Abstractions;
namespace Renci.SshNet.Security
{
internal abstract partial class KeyExchangeECDH
{
private sealed class BouncyCastleImpl : Impl
{
private readonly ECDomainParameters _domainParameters;
private readonly ECDHCBasicAgreement _keyAgreement;
public BouncyCastleImpl(X9ECParameters curveParameters)
{
_domainParameters = new ECDomainParameters(curveParameters);
_keyAgreement = new ECDHCBasicAgreement();
}
public override byte[] GenerateClientECPoint()
{
var g = new ECKeyPairGenerator();
g.Init(new ECKeyGenerationParameters(_domainParameters, CryptoAbstraction.SecureRandom));
var aKeyPair = g.GenerateKeyPair();
_keyAgreement.Init(aKeyPair.Private);
return ((ECPublicKeyParameters)aKeyPair.Public).Q.GetEncoded();
}
public override byte[] CalculateAgreement(byte[] serverECPoint)
{
var c = _domainParameters.Curve;
var q = c.DecodePoint(serverECPoint);
var publicKey = new ECPublicKeyParameters("ECDH", q, _domainParameters);
return _keyAgreement.CalculateAgreement(publicKey).ToByteArray();
}
}
}
}
+58 -31
View File
@@ -1,19 +1,28 @@
using System;
using Org.BouncyCastle.Asn1.X9;
using Org.BouncyCastle.Crypto.Agreement;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math.EC;
using Renci.SshNet.Abstractions;
using Renci.SshNet.Common;
using Renci.SshNet.Messages.Transport;
namespace Renci.SshNet.Security
{
internal abstract class KeyExchangeECDH : KeyExchangeEC
internal abstract partial class KeyExchangeECDH : KeyExchangeEC
{
#if NET8_0_OR_GREATER
private Impl _impl;
/// <summary>
/// Gets the curve.
/// </summary>
/// <value>
/// The curve.
/// </value>
protected abstract System.Security.Cryptography.ECCurve Curve { get; }
#else
private BouncyCastleImpl _impl;
#endif
/// <summary>
/// Gets the parameter of the curve.
/// </summary>
@@ -22,9 +31,6 @@ namespace Renci.SshNet.Security
/// </value>
protected abstract X9ECParameters CurveParameter { get; }
private ECDHCBasicAgreement _keyAgreement;
private ECDomainParameters _domainParameters;
/// <inheritdoc/>
public override void Start(Session session, KeyExchangeInitMessage message, bool sendClientInitMessage)
{
@@ -34,19 +40,20 @@ namespace Renci.SshNet.Security
Session.KeyExchangeEcdhReplyMessageReceived += Session_KeyExchangeEcdhReplyMessageReceived;
_domainParameters = new ECDomainParameters(CurveParameter.Curve,
CurveParameter.G,
CurveParameter.N,
CurveParameter.H,
CurveParameter.GetSeed());
#if NET8_0_OR_GREATER
if (!OperatingSystem.IsWindows() || OperatingSystem.IsWindowsVersionAtLeast(10))
{
_impl = new BclImpl(Curve);
}
else
{
_impl = new BouncyCastleImpl(CurveParameter);
}
#else
_impl = new BouncyCastleImpl(CurveParameter);
#endif
var g = new ECKeyPairGenerator();
g.Init(new ECKeyGenerationParameters(_domainParameters, CryptoAbstraction.SecureRandom));
var aKeyPair = g.GenerateKeyPair();
_keyAgreement = new ECDHCBasicAgreement();
_keyAgreement.Init(aKeyPair.Private);
_clientExchangeValue = ((ECPublicKeyParameters)aKeyPair.Public).Q.GetEncoded();
_clientExchangeValue = _impl.GenerateClientECPoint();
SendMessage(new KeyExchangeEcdhInitMessage(_clientExchangeValue));
}
@@ -86,18 +93,38 @@ namespace Renci.SshNet.Security
_hostKey = hostKey;
_signature = signature;
var cordSize = (serverExchangeValue.Length - 1) / 2;
var x = new byte[cordSize];
Buffer.BlockCopy(serverExchangeValue, 1, x, 0, x.Length); // first byte is format. should be checked and passed to bouncy castle?
var y = new byte[cordSize];
Buffer.BlockCopy(serverExchangeValue, cordSize + 1, y, 0, y.Length);
var agreement = _impl.CalculateAgreement(serverExchangeValue);
var c = (FpCurve)_domainParameters.Curve;
var q = c.CreatePoint(new Org.BouncyCastle.Math.BigInteger(1, x), new Org.BouncyCastle.Math.BigInteger(1, y));
var publicKey = new ECPublicKeyParameters("ECDH", q, _domainParameters);
SharedKey = agreement.ToBigInteger2().ToByteArray().Reverse();
}
var k1 = _keyAgreement.CalculateAgreement(publicKey);
SharedKey = k1.ToByteArray().ToBigInteger2().ToByteArray().Reverse();
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_impl?.Dispose();
}
}
private abstract class Impl : IDisposable
{
public abstract byte[] GenerateClientECPoint();
public abstract byte[] CalculateAgreement(byte[] serverECPoint);
protected virtual void Dispose(bool disposing)
{
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
}
@@ -1,4 +1,4 @@
using Org.BouncyCastle.Asn1.Sec;
using Org.BouncyCastle.Asn1.Sec;
using Org.BouncyCastle.Asn1.X9;
using Renci.SshNet.Abstractions;
@@ -15,6 +15,19 @@ namespace Renci.SshNet.Security
get { return "ecdh-sha2-nistp256"; }
}
#if NET8_0_OR_GREATER
/// <summary>
/// Gets the curve.
/// </summary>
protected override System.Security.Cryptography.ECCurve Curve
{
get
{
return System.Security.Cryptography.ECCurve.NamedCurves.nistP256;
}
}
#endif
/// <summary>
/// Gets Curve Parameter.
/// </summary>
@@ -22,7 +35,7 @@ namespace Renci.SshNet.Security
{
get
{
return SecNamedCurves.GetByName("secp256r1");
return SecNamedCurves.GetByOid(SecObjectIdentifiers.SecP256r1);
}
}
@@ -1,4 +1,4 @@
using Org.BouncyCastle.Asn1.Sec;
using Org.BouncyCastle.Asn1.Sec;
using Org.BouncyCastle.Asn1.X9;
using Renci.SshNet.Abstractions;
@@ -15,6 +15,19 @@ namespace Renci.SshNet.Security
get { return "ecdh-sha2-nistp384"; }
}
#if NET8_0_OR_GREATER
/// <summary>
/// Gets the curve.
/// </summary>
protected override System.Security.Cryptography.ECCurve Curve
{
get
{
return System.Security.Cryptography.ECCurve.NamedCurves.nistP384;
}
}
#endif
/// <summary>
/// Gets Curve Parameter.
/// </summary>
@@ -22,7 +35,7 @@ namespace Renci.SshNet.Security
{
get
{
return SecNamedCurves.GetByName("secp384r1");
return SecNamedCurves.GetByOid(SecObjectIdentifiers.SecP384r1);
}
}
@@ -1,4 +1,4 @@
using Org.BouncyCastle.Asn1.Sec;
using Org.BouncyCastle.Asn1.Sec;
using Org.BouncyCastle.Asn1.X9;
using Renci.SshNet.Abstractions;
@@ -15,6 +15,19 @@ namespace Renci.SshNet.Security
get { return "ecdh-sha2-nistp521"; }
}
#if NET8_0_OR_GREATER
/// <summary>
/// Gets the curve.
/// </summary>
protected override System.Security.Cryptography.ECCurve Curve
{
get
{
return System.Security.Cryptography.ECCurve.NamedCurves.nistP521;
}
}
#endif
/// <summary>
/// Gets Curve Parameter.
/// </summary>
@@ -22,7 +35,7 @@ namespace Renci.SshNet.Security
{
get
{
return SecNamedCurves.GetByName("secp521r1");
return SecNamedCurves.GetByOid(SecObjectIdentifiers.SecP521r1);
}
}