浏览代码

[ECDsa] Falls back to use BouncyCastle if BCL (Mono) doesn't support (#1461)

* Use BouncyCastle ECDsa when runtime is Mono

* Falls back to use BouncyCastle if CngKey.Import throws NotImplementedException (in Mono)

* Take NETStandard into consideration

* Adjust some comments

* Change #if NETFRAEWORK to #if NET462 for CngKey

* Separate implementations

* Consolidate Ecdsa property and HashAlgorithm property

* Rename Import_Cng and Import_Bcl to Import; Rename Export_Cng and Export_Bcl to Export;

* Add comments

* refactor

* add host key tests

---------

Co-authored-by: Robert Hague <rh@johnstreetcapital.com>
Co-authored-by: Rob Hague <rob.hague00@gmail.com>
Scott Xu 1 年之前
父节点
当前提交
fe827a529a

+ 1 - 1
appveyor.yml

@@ -32,7 +32,7 @@ for:
     # some coverage until a proper solution for running the .NET Framework integration tests in CI is found.
     # Running all the tests causes problems which are not worth solving in this rare configuration.
     # See https://github.com/sshnet/SSH.NET/pull/1462 and related links
-    - 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~Ecdh|Name~Zlib|Name~Gcm" 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~Ecdh|Name~ECDsa|Name~Zlib|Name~Gcm" test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj
 
 -
   matrix:

+ 1 - 1
src/Renci.SshNet/Common/SshDataStream.cs

@@ -56,7 +56,7 @@ namespace Renci.SshNet.Common
             }
         }
 
-#if NET462 || NETSTANDARD2_0
+#if NETFRAMEWORK || NETSTANDARD2_0
         private int Read(Span<byte> buffer)
         {
             var sharedBuffer = System.Buffers.ArrayPool<byte>.Shared.Rent(buffer.Length);

+ 1 - 6
src/Renci.SshNet/Security/Cryptography/DsaKey.cs

@@ -41,12 +41,7 @@ namespace Renci.SshNet.Security
         /// </summary>
         public BigInteger X { get; }
 
-        /// <summary>
-        /// Gets the length of the key in bits.
-        /// </summary>
-        /// <value>
-        /// The bit-length of the key.
-        /// </value>
+        /// <inheritdoc/>
         public override int KeyLength
         {
             get

+ 1 - 6
src/Renci.SshNet/Security/Cryptography/ED25519Key.cs

@@ -40,12 +40,7 @@ namespace Renci.SshNet.Security
             }
         }
 
-        /// <summary>
-        /// Gets the length of the key.
-        /// </summary>
-        /// <value>
-        /// The length of the key.
-        /// </value>
+        /// <inheritdoc/>
         public override int KeyLength
         {
             get

+ 4 - 45
src/Renci.SshNet/Security/Cryptography/EcdsaDigitalSignature.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Globalization;
 
 using Renci.SshNet.Common;
 
@@ -11,7 +10,6 @@ namespace Renci.SshNet.Security.Cryptography
     public class EcdsaDigitalSignature : DigitalSignature, IDisposable
     {
         private readonly EcdsaKey _key;
-        private bool _isDisposed;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="EcdsaDigitalSignature" /> class.
@@ -41,13 +39,8 @@ namespace Renci.SshNet.Security.Cryptography
             // for 521 sig_size is 132
             var sig_size = _key.KeyLength == 521 ? 132 : _key.KeyLength / 4;
             var ssh_data = new SshDataSignature(signature, sig_size);
-#if NETFRAMEWORK
-            var ecdsa = _key.Ecdsa;
-            ecdsa.HashAlgorithm = _key.HashAlgorithm;
-            return ecdsa.VerifyData(input, ssh_data.Signature);
-#else
-            return _key.Ecdsa.VerifyData(input, ssh_data.Signature, _key.HashAlgorithm);
-#endif
+
+            return _key._impl.Verify(input, ssh_data.Signature);
         }
 
         /// <summary>
@@ -59,13 +52,8 @@ namespace Renci.SshNet.Security.Cryptography
         /// </returns>
         public override byte[] Sign(byte[] input)
         {
-#if NETFRAMEWORK
-            var ecdsa = _key.Ecdsa;
-            ecdsa.HashAlgorithm = _key.HashAlgorithm;
-            var signed = ecdsa.SignData(input);
-#else
-            var signed = _key.Ecdsa.SignData(input, _key.HashAlgorithm);
-#endif
+            var signed = _key._impl.Sign(input);
+
             var ssh_data = new SshDataSignature(signed.Length) { Signature = signed };
             return ssh_data.GetBytes();
         }
@@ -85,23 +73,6 @@ namespace Renci.SshNet.Security.Cryptography
         /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
         protected virtual void Dispose(bool disposing)
         {
-            if (_isDisposed)
-            {
-                return;
-            }
-
-            if (disposing)
-            {
-                _isDisposed = true;
-            }
-        }
-
-        /// <summary>
-        /// Finalizes an instance of the <see cref="EcdsaDigitalSignature"/> class.
-        /// </summary>
-        ~EcdsaDigitalSignature()
-        {
-            Dispose(disposing: false);
         }
 
         private sealed class SshDataSignature : SshData
@@ -155,18 +126,6 @@ namespace Renci.SshNet.Security.Cryptography
                 WriteBinaryString(_signature_s.ToBigInteger2().ToByteArray().Reverse());
             }
 
-            public new byte[] ReadBinary()
-            {
-                var length = ReadUInt32();
-
-                if (length > int.MaxValue)
-                {
-                    throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Strings longer than {0} is not supported.", int.MaxValue));
-                }
-
-                return ReadBytes((int)length);
-            }
-
             protected override int BufferCapacity
             {
                 get

+ 82 - 0
src/Renci.SshNet/Security/Cryptography/EcdsaKey.BclImpl.cs

@@ -0,0 +1,82 @@
+#if !NET462
+#nullable enable
+using System;
+using System.Security.Cryptography;
+
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Security
+{
+    public partial class EcdsaKey : Key, IDisposable
+    {
+        private sealed class BclImpl : Impl
+        {
+            private readonly HashAlgorithmName _hashAlgorithmName;
+
+            public BclImpl(string curve_oid, int cord_size, byte[] qx, byte[] qy, byte[]? privatekey)
+            {
+                var curve = ECCurve.CreateFromValue(curve_oid);
+                var parameter = new ECParameters
+                {
+                    Curve = curve
+                };
+
+                parameter.Q.X = qx;
+                parameter.Q.Y = qy;
+
+                if (privatekey != null)
+                {
+                    parameter.D = privatekey.TrimLeadingZeros().Pad(cord_size);
+                    PrivateKey = parameter.D;
+                }
+
+                Ecdsa = ECDsa.Create(parameter);
+
+                _hashAlgorithmName = KeyLength switch
+                {
+                    <= 256 => HashAlgorithmName.SHA256,
+                    <= 384 => HashAlgorithmName.SHA384,
+                    _ => HashAlgorithmName.SHA512,
+                };
+            }
+
+            public override byte[]? PrivateKey { get; }
+
+            public override ECDsa Ecdsa { get; }
+
+            public override int KeyLength
+            {
+                get
+                {
+                    return Ecdsa.KeySize;
+                }
+            }
+
+            public override byte[] Sign(byte[] input)
+            {
+                return Ecdsa.SignData(input, _hashAlgorithmName);
+            }
+
+            public override bool Verify(byte[] input, byte[] signature)
+            {
+                return Ecdsa.VerifyData(input, signature, _hashAlgorithmName);
+            }
+
+            public override void Export(out byte[] qx, out byte[] qy)
+            {
+                var parameter = Ecdsa.ExportParameters(includePrivateParameters: false);
+                qx = parameter.Q.X!;
+                qy = parameter.Q.Y!;
+            }
+
+            protected override void Dispose(bool disposing)
+            {
+                if (disposing)
+                {
+                    Ecdsa.Dispose();
+                }
+            }
+        }
+    }
+}
+#endif

+ 108 - 0
src/Renci.SshNet/Security/Cryptography/EcdsaKey.BouncyCastleImpl.cs

@@ -0,0 +1,108 @@
+#if !NET
+#nullable enable
+using System.Security.Cryptography;
+
+using Org.BouncyCastle.Asn1;
+using Org.BouncyCastle.Asn1.Sec;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Digests;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Crypto.Signers;
+
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Security
+{
+    public partial class EcdsaKey
+    {
+        private sealed class BouncyCastleImpl : Impl
+        {
+            private readonly ECPublicKeyParameters _publicKeyParameters;
+            private readonly ECPrivateKeyParameters? _privateKeyParameters;
+            private readonly DsaDigestSigner _signer;
+
+            public BouncyCastleImpl(string curve_oid, byte[] qx, byte[] qy, byte[]? privatekey)
+            {
+                DerObjectIdentifier oid;
+                IDigest digest;
+                switch (curve_oid)
+                {
+                    case ECDSA_P256_OID_VALUE:
+                        oid = SecObjectIdentifiers.SecP256r1;
+                        digest = new Sha256Digest();
+                        KeyLength = 256;
+                        break;
+                    case ECDSA_P384_OID_VALUE:
+                        oid = SecObjectIdentifiers.SecP384r1;
+                        digest = new Sha384Digest();
+                        KeyLength = 384;
+                        break;
+                    case ECDSA_P521_OID_VALUE:
+                        oid = SecObjectIdentifiers.SecP521r1;
+                        digest = new Sha512Digest();
+                        KeyLength = 521;
+                        break;
+                    default:
+                        throw new SshException("Unexpected OID: " + curve_oid);
+                }
+
+                _signer = new DsaDigestSigner(new ECDsaSigner(), digest, PlainDsaEncoding.Instance);
+
+                var x9ECParameters = SecNamedCurves.GetByOid(oid);
+                var domainParameter = new ECNamedDomainParameters(oid, x9ECParameters);
+
+                if (privatekey != null)
+                {
+                    _privateKeyParameters = new ECPrivateKeyParameters(
+                        new Org.BouncyCastle.Math.BigInteger(1, privatekey),
+                        domainParameter);
+
+                    _publicKeyParameters = new ECPublicKeyParameters(
+                        domainParameter.G.Multiply(_privateKeyParameters.D).Normalize(),
+                        domainParameter);
+                }
+                else
+                {
+                    _publicKeyParameters = new ECPublicKeyParameters(
+                        x9ECParameters.Curve.CreatePoint(
+                            new Org.BouncyCastle.Math.BigInteger(1, qx),
+                            new Org.BouncyCastle.Math.BigInteger(1, qy)),
+                        domainParameter);
+                }
+            }
+
+            public override byte[]? PrivateKey { get; }
+
+            public override ECDsa? Ecdsa { get; }
+
+            public override int KeyLength { get; }
+
+            public override byte[] Sign(byte[] input)
+            {
+                _signer.Init(forSigning: true, _privateKeyParameters);
+                _signer.BlockUpdate(input, 0, input.Length);
+
+                return _signer.GenerateSignature();
+            }
+
+            public override bool Verify(byte[] input, byte[] signature)
+            {
+                _signer.Init(forSigning: false, _publicKeyParameters);
+                _signer.BlockUpdate(input, 0, input.Length);
+
+                return _signer.VerifySignature(signature);
+            }
+
+            public override void Export(out byte[] qx, out byte[] qy)
+            {
+                qx = _publicKeyParameters.Q.XCoord.GetEncoded();
+                qy = _publicKeyParameters.Q.YCoord.GetEncoded();
+            }
+
+            protected override void Dispose(bool disposing)
+            {
+            }
+        }
+    }
+}
+#endif

+ 137 - 0
src/Renci.SshNet/Security/Cryptography/EcdsaKey.CngImpl.cs

@@ -0,0 +1,137 @@
+#if NET462
+#nullable enable
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Security.Cryptography;
+
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Security
+{
+    public partial class EcdsaKey : Key, IDisposable
+    {
+        private sealed class CngImpl : Impl
+        {
+            private readonly CngKey _key;
+            private readonly HashAlgorithmName _hashAlgorithmName;
+
+            private enum KeyBlobMagicNumber
+            {
+                BCRYPT_ECDSA_PUBLIC_P256_MAGIC = 0x31534345,
+                BCRYPT_ECDSA_PRIVATE_P256_MAGIC = 0x32534345,
+                BCRYPT_ECDSA_PUBLIC_P384_MAGIC = 0x33534345,
+                BCRYPT_ECDSA_PRIVATE_P384_MAGIC = 0x34534345,
+                BCRYPT_ECDSA_PUBLIC_P521_MAGIC = 0x35534345,
+                BCRYPT_ECDSA_PRIVATE_P521_MAGIC = 0x36534345,
+            }
+
+            public CngImpl(string curve_oid, int cord_size, byte[] qx, byte[] qy, byte[]? privatekey)
+            {
+                KeyBlobMagicNumber curve_magic;
+
+                switch (curve_oid)
+                {
+                    case ECDSA_P256_OID_VALUE:
+                        curve_magic = privatekey != null
+                            ? KeyBlobMagicNumber.BCRYPT_ECDSA_PRIVATE_P256_MAGIC
+                            : KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P256_MAGIC;
+                        break;
+                    case ECDSA_P384_OID_VALUE:
+                        curve_magic = privatekey != null
+                            ? KeyBlobMagicNumber.BCRYPT_ECDSA_PRIVATE_P384_MAGIC
+                            : KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P384_MAGIC;
+                        break;
+                    case ECDSA_P521_OID_VALUE:
+                        curve_magic = privatekey != null
+                            ? KeyBlobMagicNumber.BCRYPT_ECDSA_PRIVATE_P521_MAGIC
+                            : KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P521_MAGIC;
+                        break;
+                    default:
+                        throw new SshException("Unknown: " + curve_oid);
+                }
+
+                if (privatekey != null)
+                {
+                    privatekey = privatekey.Pad(cord_size);
+                    PrivateKey = privatekey;
+                }
+
+                var blobSize = sizeof(int) + sizeof(int) + qx.Length + qy.Length;
+                if (privatekey != null)
+                {
+                    blobSize += privatekey.Length;
+                }
+
+                var blob = new byte[blobSize];
+                using (var bw = new BinaryWriter(new MemoryStream(blob)))
+                {
+                    bw.Write((int)curve_magic);
+                    bw.Write(cord_size);
+                    bw.Write(qx); // q.x
+                    bw.Write(qy); // q.y
+                    if (privatekey != null)
+                    {
+                        bw.Write(privatekey); // d
+                    }
+                }
+
+                _key = CngKey.Import(blob, privatekey is null ? CngKeyBlobFormat.EccPublicBlob : CngKeyBlobFormat.EccPrivateBlob);
+                Ecdsa = new ECDsaCng(_key);
+
+                _hashAlgorithmName = KeyLength switch
+                {
+                    <= 256 => HashAlgorithmName.SHA256,
+                    <= 384 => HashAlgorithmName.SHA384,
+                    _ => HashAlgorithmName.SHA512,
+                };
+            }
+
+            public override byte[]? PrivateKey { get; }
+
+            public override ECDsa Ecdsa { get; }
+
+            public override int KeyLength
+            {
+                get
+                {
+                    return Ecdsa.KeySize;
+                }
+            }
+
+            public override byte[] Sign(byte[] input)
+            {
+                return Ecdsa.SignData(input, _hashAlgorithmName);
+            }
+
+            public override bool Verify(byte[] input, byte[] signature)
+            {
+                return Ecdsa.VerifyData(input, signature, _hashAlgorithmName);
+            }
+
+            public override void Export(out byte[] qx, out byte[] qy)
+            {
+                var blob = _key.Export(CngKeyBlobFormat.EccPublicBlob);
+
+                using (var br = new BinaryReader(new MemoryStream(blob)))
+                {
+                    var magic = br.ReadInt32();
+                    Debug.Assert(Enum.IsDefined(typeof(KeyBlobMagicNumber), magic), magic.ToString("x"));
+                    var cbKey = br.ReadInt32();
+                    qx = br.ReadBytes(cbKey);
+                    qy = br.ReadBytes(cbKey);
+                }
+            }
+
+            protected override void Dispose(bool disposing)
+            {
+                if (disposing)
+                {
+                    Ecdsa.Dispose();
+                    _key.Dispose();
+                }
+            }
+        }
+    }
+}
+#endif

+ 100 - 259
src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs

@@ -1,9 +1,6 @@
-using System;
-#if NETFRAMEWORK
-using System.Globalization;
-using System.IO;
-using System.Runtime.InteropServices;
-#endif // NETFRAMEWORK
+#nullable enable
+using System;
+using System.Diagnostics;
 using System.Security.Cryptography;
 using System.Text;
 
@@ -15,7 +12,7 @@ namespace Renci.SshNet.Security
     /// <summary>
     /// Contains ECDSA (ecdsa-sha2-nistp{256,384,521}) private and public key.
     /// </summary>
-    public class EcdsaKey : Key, IDisposable
+    public partial class EcdsaKey : Key, IDisposable
     {
 #pragma warning disable SA1310 // Field names should not contain underscore
         private const string ECDSA_P256_OID_VALUE = "1.2.840.10045.3.1.7"; // Also called nistP256 or secP256r1
@@ -23,39 +20,15 @@ namespace Renci.SshNet.Security
         private const string ECDSA_P521_OID_VALUE = "1.3.132.0.35"; // Also called nistP521or secP521r1
 #pragma warning restore SA1310 // Field names should not contain underscore
 
-        private EcdsaDigitalSignature _digitalSignature;
-        private bool _isDisposed;
+        private static readonly BigInteger Encoded256 = new BigInteger("nistp256"u8.ToArray().Reverse());
+        private static readonly BigInteger Encoded384 = new BigInteger("nistp384"u8.ToArray().Reverse());
+        private static readonly BigInteger Encoded521 = new BigInteger("nistp521"u8.ToArray().Reverse());
 
-#if NETFRAMEWORK
-        private CngKey _key;
+        private EcdsaDigitalSignature? _digitalSignature;
 
-        internal enum KeyBlobMagicNumber
-        {
-            BCRYPT_ECDSA_PUBLIC_P256_MAGIC = 0x31534345,
-            BCRYPT_ECDSA_PRIVATE_P256_MAGIC = 0x32534345,
-            BCRYPT_ECDSA_PUBLIC_P384_MAGIC = 0x33534345,
-            BCRYPT_ECDSA_PRIVATE_P384_MAGIC = 0x34534345,
-            BCRYPT_ECDSA_PUBLIC_P521_MAGIC = 0x35534345,
-            BCRYPT_ECDSA_PRIVATE_P521_MAGIC = 0x36534345,
-
-            BCRYPT_ECDH_PUBLIC_P256_MAGIC = 0x314B4345,
-            BCRYPT_ECDH_PRIVATE_P256_MAGIC = 0x324B4345,
-            BCRYPT_ECDH_PUBLIC_P384_MAGIC = 0x334B4345,
-            BCRYPT_ECDH_PRIVATE_P384_MAGIC = 0x344B4345,
-            BCRYPT_ECDH_PUBLIC_P521_MAGIC = 0x354B4345,
-            BCRYPT_ECDH_PRIVATE_P521_MAGIC = 0x364B4345,
-
-            BCRYPT_ECDH_PUBLIC_GENERIC_MAGIC = 0x504B4345,
-            BCRYPT_ECDH_PRIVATE_GENERIC_MAGIC = 0x564B4345,
-        }
-
-        [StructLayout(LayoutKind.Sequential)]
-        internal struct BCRYPT_ECCKEY_BLOB
-        {
-            internal KeyBlobMagicNumber Magic;
-            internal int CbKey;
-        }
-#endif
+#pragma warning disable SA1401 // Fields should be private; internal readonly
+        internal readonly Impl _impl;
+#pragma warning restore SA1401 // Fields should be private
 
         /// <summary>
         /// Gets the SSH name of the ECDSA Key.
@@ -68,28 +41,6 @@ namespace Renci.SshNet.Security
             return string.Format("ecdsa-sha2-nistp{0}", KeyLength);
         }
 
-#if NETFRAMEWORK
-        /// <summary>
-        /// Gets the HashAlgorithm to use.
-        /// </summary>
-        public CngAlgorithm HashAlgorithm
-        {
-            get
-            {
-                switch (Ecdsa.KeySize)
-                {
-                    case 256:
-                        return CngAlgorithm.Sha256;
-                    case 384:
-                        return CngAlgorithm.Sha384;
-                    case 521:
-                        return CngAlgorithm.Sha512;
-                    default:
-                        throw new SshException("Unknown KeySize: " + Ecdsa.KeySize.ToString(CultureInfo.InvariantCulture));
-                }
-            }
-        }
-#else
         /// <summary>
         /// Gets the HashAlgorithm to use.
         /// </summary>
@@ -110,25 +61,44 @@ namespace Renci.SshNet.Security
                 }
             }
         }
+
+        internal abstract class Impl : IDisposable
+        {
+            public abstract int KeyLength { get; }
+
+            public abstract byte[]? PrivateKey { get; }
+
+#if NET
+            public abstract ECDsa Ecdsa { get; }
+#else
+            public abstract ECDsa? Ecdsa { get; }
 #endif
 
-        /// <summary>
-        /// Gets the length of the key.
-        /// </summary>
-        /// <value>
-        /// The length of the key.
-        /// </value>
+            public abstract bool Verify(byte[] input, byte[] signature);
+
+            public abstract byte[] Sign(byte[] input);
+
+            public abstract void Export(out byte[] qx, out byte[] qy);
+
+            protected abstract void Dispose(bool disposing);
+
+            public void Dispose()
+            {
+                Dispose(disposing: true);
+                GC.SuppressFinalize(this);
+            }
+        }
+
+        /// <inheritdoc/>
         public override int KeyLength
         {
             get
             {
-                return Ecdsa.KeySize;
+                return _impl.KeyLength;
             }
         }
 
-        /// <summary>
-        /// Gets the digital signature.
-        /// </summary>
+        /// <inheritdoc/>
         protected internal override DigitalSignature DigitalSignature
         {
             get
@@ -150,88 +120,60 @@ namespace Renci.SshNet.Security
         {
             get
             {
-                byte[] curve;
-                byte[] qx;
-                byte[] qy;
-#if NETFRAMEWORK
-                var blob = _key.Export(CngKeyBlobFormat.EccPublicBlob);
-
-                KeyBlobMagicNumber magic;
-                using (var br = new BinaryReader(new MemoryStream(blob)))
-                {
-                    magic = (KeyBlobMagicNumber)br.ReadInt32();
-                    var cbKey = br.ReadInt32();
-                    qx = br.ReadBytes(cbKey);
-                    qy = br.ReadBytes(cbKey);
-                }
-
-#pragma warning disable IDE0010 // Add missing cases
-                switch (magic)
+                BigInteger curve;
+                switch (KeyLength)
                 {
-                    case KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P256_MAGIC:
-                        curve = Encoding.ASCII.GetBytes("nistp256");
-                        break;
-                    case KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P384_MAGIC:
-                        curve = Encoding.ASCII.GetBytes("nistp384");
+                    case 256:
+                        curve = Encoded256;
                         break;
-                    case KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P521_MAGIC:
-                        curve = Encoding.ASCII.GetBytes("nistp521");
+                    case 384:
+                        curve = Encoded384;
                         break;
                     default:
-                        throw new SshException("Unexpected Curve Magic: " + magic);
-                }
-#pragma warning restore IDE0010 // Add missing cases
-#else
-                var parameter = Ecdsa.ExportParameters(includePrivateParameters: false);
-                qx = parameter.Q.X;
-                qy = parameter.Q.Y;
-                switch (parameter.Curve.Oid.FriendlyName)
-                {
-                    case "ECDSA_P256":
-                    case "nistP256":
-                        curve = Encoding.ASCII.GetBytes("nistp256");
-                        break;
-                    case "ECDSA_P384":
-                    case "nistP384":
-                        curve = Encoding.ASCII.GetBytes("nistp384");
+                        Debug.Assert(KeyLength == 521);
+                        curve = Encoded521;
                         break;
-                    case "ECDSA_P521":
-                    case "nistP521":
-                        curve = Encoding.ASCII.GetBytes("nistp521");
-                        break;
-                    default:
-                        throw new SshException("Unexpected Curve Name: " + parameter.Curve.Oid.FriendlyName);
                 }
-#endif
+
+                _impl.Export(out var qx, out var qy);
 
                 // Make ECPoint from x and y
                 // Prepend 04 (uncompressed format) + qx-bytes + qy-bytes
                 var q = new byte[1 + qx.Length + qy.Length];
-                Buffer.SetByte(q, 0, 4);
+                q[0] = 0x4;
                 Buffer.BlockCopy(qx, 0, q, 1, qx.Length);
                 Buffer.BlockCopy(qy, 0, q, qx.Length + 1, qy.Length);
 
                 // returns Curve-Name and x/y as ECPoint
-                return new[] { new BigInteger(curve.Reverse()), new BigInteger(q.Reverse()) };
+                return new[] { curve, new BigInteger(q.Reverse()) };
             }
         }
 
         /// <summary>
         /// Gets the PrivateKey Bytes.
         /// </summary>
-        public byte[] PrivateKey { get; private set; }
+        public byte[]? PrivateKey
+        {
+            get
+            {
+                return _impl.PrivateKey;
+            }
+        }
 
-#if NETFRAMEWORK
         /// <summary>
         /// Gets the <see cref="ECDsa"/> object.
         /// </summary>
-        public ECDsaCng Ecdsa { get; private set; }
+#if NET
+        public ECDsa Ecdsa
 #else
-        /// <summary>
-        /// Gets the <see cref="ECDsa"/> object.
-        /// </summary>
-        public ECDsa Ecdsa { get; private set; }
+        public ECDsa? Ecdsa
 #endif
+        {
+            get
+            {
+                return _impl.Ecdsa;
+            }
+        }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="EcdsaKey"/> class.
@@ -253,7 +195,7 @@ namespace Renci.SshNet.Security
             var curve_oid = GetCurveOid(curve_s);
 
             var publickey = publicKeyData.Keys[1].ToByteArray().Reverse();
-            Import(curve_oid, publickey, privatekey: null);
+            _impl = Import(curve_oid, publickey, privatekey: null);
         }
 
         /// <summary>
@@ -264,7 +206,7 @@ namespace Renci.SshNet.Security
         /// <param name="privatekey">Value of privatekey.</param>
         public EcdsaKey(string curve, byte[] publickey, byte[] privatekey)
         {
-            Import(GetCurveOid(curve), publickey, privatekey);
+            _impl = Import(GetCurveOid(curve), publickey, privatekey);
         }
 
         /// <summary>
@@ -317,53 +259,13 @@ namespace Renci.SshNet.Security
             var pubkey_der = new DerData(construct, construct: true);
             var pubkey = pubkey_der.ReadBitString().TrimLeadingZeros();
 
-            Import(OidByteArrayToString(curve), pubkey, privatekey);
+            _impl = Import(OidByteArrayToString(curve), pubkey, privatekey);
         }
 
-        private void Import(string curve_oid, byte[] publickey, byte[] privatekey)
+#pragma warning disable CA1859 // Use concrete types when possible for improved performance
+        private static Impl Import(string curve_oid, byte[] publickey, byte[]? privatekey)
+#pragma warning restore CA1859 // Use concrete types when possible for improved performance
         {
-#if NETFRAMEWORK
-            KeyBlobMagicNumber curve_magic;
-
-            switch (GetCurveName(curve_oid))
-            {
-                case "nistp256":
-                    if (privatekey != null)
-                    {
-                        curve_magic = KeyBlobMagicNumber.BCRYPT_ECDSA_PRIVATE_P256_MAGIC;
-                    }
-                    else
-                    {
-                        curve_magic = KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P256_MAGIC;
-                    }
-
-                    break;
-                case "nistp384":
-                    if (privatekey != null)
-                    {
-                        curve_magic = KeyBlobMagicNumber.BCRYPT_ECDSA_PRIVATE_P384_MAGIC;
-                    }
-                    else
-                    {
-                        curve_magic = KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P384_MAGIC;
-                    }
-
-                    break;
-                case "nistp521":
-                    if (privatekey != null)
-                    {
-                        curve_magic = KeyBlobMagicNumber.BCRYPT_ECDSA_PRIVATE_P521_MAGIC;
-                    }
-                    else
-                    {
-                        curve_magic = KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P521_MAGIC;
-                    }
-
-                    break;
-                default:
-                    throw new SshException("Unknown: " + curve_oid);
-            }
-
             // ECPoint as BigInteger(2)
             var cord_size = (publickey.Length - 1) / 2;
             var qx = new byte[cord_size];
@@ -372,94 +274,45 @@ namespace Renci.SshNet.Security
             var qy = new byte[cord_size];
             Buffer.BlockCopy(publickey, cord_size + 1, qy, 0, qy.Length);
 
-            if (privatekey != null)
-            {
-                privatekey = privatekey.Pad(cord_size);
-                PrivateKey = privatekey;
-            }
-
-            var headerSize = Marshal.SizeOf<BCRYPT_ECCKEY_BLOB>();
-            var blobSize = headerSize + qx.Length + qy.Length;
-            if (privatekey != null)
-            {
-                blobSize += privatekey.Length;
-            }
-
-            var blob = new byte[blobSize];
-            using (var bw = new BinaryWriter(new MemoryStream(blob)))
-            {
-                bw.Write((int)curve_magic);
-                bw.Write(cord_size);
-                bw.Write(qx); // q.x
-                bw.Write(qy); // q.y
-                if (privatekey != null)
-                {
-                    bw.Write(privatekey); // d
-                }
-            }
-
-            _key = CngKey.Import(blob, privatekey is null ? CngKeyBlobFormat.EccPublicBlob : CngKeyBlobFormat.EccPrivateBlob);
-
-            Ecdsa = new ECDsaCng(_key);
+#if NET
+            return new BclImpl(curve_oid, cord_size, qx, qy, privatekey);
 #else
-            var curve = ECCurve.CreateFromValue(curve_oid);
-            var parameter = new ECParameters
+            try
             {
-                Curve = curve
-            };
-
-            // ECPoint as BigInteger(2)
-            var cord_size = (publickey.Length - 1) / 2;
-            var qx = new byte[cord_size];
-            Buffer.BlockCopy(publickey, 1, qx, 0, qx.Length);
-
-            var qy = new byte[cord_size];
-            Buffer.BlockCopy(publickey, cord_size + 1, qy, 0, qy.Length);
-
-            parameter.Q.X = qx;
-            parameter.Q.Y = qy;
-
-            if (privatekey != null)
+#if NET462
+                return new CngImpl(curve_oid, cord_size, qx, qy, privatekey);
+#else
+                return new BclImpl(curve_oid, cord_size, qx, qy, privatekey);
+#endif
+            }
+            catch (NotImplementedException)
             {
-                parameter.D = privatekey.TrimLeadingZeros().Pad(cord_size);
-                PrivateKey = parameter.D;
+                // Mono doesn't implement ECDsa.Create()
+                // See https://github.com/mono/mono/blob/main/mcs/class/referencesource/System.Core/System/Security/Cryptography/ECDsa.cs#L32
+                return new BouncyCastleImpl(curve_oid, qx, qy, privatekey);
             }
-
-            Ecdsa = ECDsa.Create(parameter);
 #endif
         }
 
         private static string GetCurveOid(string curve_s)
         {
-            switch (curve_s.ToUpperInvariant())
+            if (string.Equals(curve_s, "nistp256", StringComparison.OrdinalIgnoreCase))
             {
-                case "NISTP256":
-                    return ECDSA_P256_OID_VALUE;
-                case "NISTP384":
-                    return ECDSA_P384_OID_VALUE;
-                case "NISTP521":
-                    return ECDSA_P521_OID_VALUE;
-                default:
-                    throw new SshException("Unexpected Curve Name: " + curve_s);
+                return ECDSA_P256_OID_VALUE;
             }
-        }
 
-#if NETFRAMEWORK
-        private static string GetCurveName(string oid)
-        {
-            switch (oid)
+            if (string.Equals(curve_s, "nistp384", StringComparison.OrdinalIgnoreCase))
             {
-                case ECDSA_P256_OID_VALUE:
-                    return "nistp256";
-                case ECDSA_P384_OID_VALUE:
-                    return "nistp384";
-                case ECDSA_P521_OID_VALUE:
-                    return "nistp521";
-                default:
-                    throw new SshException("Unexpected OID: " + oid);
+                return ECDSA_P384_OID_VALUE;
             }
+
+            if (string.Equals(curve_s, "nistp521", StringComparison.OrdinalIgnoreCase))
+            {
+                return ECDSA_P521_OID_VALUE;
+            }
+
+            throw new SshException("Unexpected Curve Name: " + curve_s);
         }
-#endif // NETFRAMEWORK
 
         private static string OidByteArrayToString(byte[] oid)
         {
@@ -505,23 +358,11 @@ namespace Renci.SshNet.Security
         /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
         protected virtual void Dispose(bool disposing)
         {
-            if (_isDisposed)
-            {
-                return;
-            }
-
             if (disposing)
             {
-                _isDisposed = true;
+                _digitalSignature?.Dispose();
+                _impl.Dispose();
             }
         }
-
-        /// <summary>
-        /// Finalizes an instance of the <see cref="EcdsaKey"/> class.
-        /// </summary>
-        ~EcdsaKey()
-        {
-            Dispose(disposing: false);
-        }
     }
 }

+ 2 - 2
src/Renci.SshNet/Security/Cryptography/Key.cs

@@ -22,10 +22,10 @@ namespace Renci.SshNet.Security
         public abstract BigInteger[] Public { get; }
 
         /// <summary>
-        /// Gets the length of the key.
+        /// Gets the length of the key in bits.
         /// </summary>
         /// <value>
-        /// The length of the key.
+        /// The bit-length of the key.
         /// </value>
         public abstract int KeyLength { get; }
 

+ 1 - 6
src/Renci.SshNet/Security/Cryptography/RsaKey.cs

@@ -91,12 +91,7 @@ namespace Renci.SshNet.Security
         /// </value>
         public BigInteger InverseQ { get; }
 
-        /// <summary>
-        /// Gets the length of the key in bits.
-        /// </summary>
-        /// <value>
-        /// The bit-length of the key.
-        /// </value>
+        /// <inheritdoc/>
         public override int KeyLength
         {
             get

+ 25 - 7
test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs

@@ -26,34 +26,52 @@ namespace Renci.SshNet.IntegrationTests
         [TestMethod]
         public void SshDss()
         {
-            DoTest(HostKeyAlgorithm.SshDss, HostKeyFile.Dsa, 1024);
+            DoTest(HostKeyAlgorithm.SshDss, HostKeyFile.Dsa);
         }
 
         [TestMethod]
         public void SshRsa()
         {
-            DoTest(HostKeyAlgorithm.SshRsa, HostKeyFile.Rsa, 3072);
+            DoTest(HostKeyAlgorithm.SshRsa, HostKeyFile.Rsa);
         }
 
         [TestMethod]
         public void SshRsaSha256()
         {
-            DoTest(HostKeyAlgorithm.RsaSha2256, HostKeyFile.Rsa, 3072);
+            DoTest(HostKeyAlgorithm.RsaSha2256, HostKeyFile.Rsa);
         }
 
         [TestMethod]
         public void SshRsaSha512()
         {
-            DoTest(HostKeyAlgorithm.RsaSha2512, HostKeyFile.Rsa, 3072);
+            DoTest(HostKeyAlgorithm.RsaSha2512, HostKeyFile.Rsa);
         }
 
         [TestMethod]
         public void SshEd25519()
         {
-            DoTest(HostKeyAlgorithm.SshEd25519, HostKeyFile.Ed25519, 256);
+            DoTest(HostKeyAlgorithm.SshEd25519, HostKeyFile.Ed25519);
         }
 
-        private void DoTest(HostKeyAlgorithm hostKeyAlgorithm, HostKeyFile hostKeyFile, int keyLength)
+        [TestMethod]
+        public void Ecdsa256()
+        {
+            DoTest(HostKeyAlgorithm.EcdsaSha2Nistp256, HostKeyFile.Ecdsa256);
+        }
+
+        [TestMethod]
+        public void Ecdsa384()
+        {
+            DoTest(HostKeyAlgorithm.EcdsaSha2Nistp384, HostKeyFile.Ecdsa384);
+        }
+
+        [TestMethod]
+        public void Ecdsa521()
+        {
+            DoTest(HostKeyAlgorithm.EcdsaSha2Nistp521, HostKeyFile.Ecdsa521);
+        }
+
+        private void DoTest(HostKeyAlgorithm hostKeyAlgorithm, HostKeyFile hostKeyFile)
         {
             _remoteSshdConfig.ClearHostKeyAlgorithms()
                              .AddHostKeyAlgorithm(hostKeyAlgorithm)
@@ -73,7 +91,7 @@ namespace Renci.SshNet.IntegrationTests
 
             Assert.IsNotNull(hostKeyEventsArgs);
             Assert.AreEqual(hostKeyAlgorithm.Name, hostKeyEventsArgs.HostKeyName);
-            Assert.AreEqual(keyLength, hostKeyEventsArgs.KeyLength);
+            Assert.AreEqual(hostKeyFile.KeyLength, hostKeyEventsArgs.KeyLength);
             CollectionAssert.AreEqual(hostKeyFile.FingerPrint, hostKeyEventsArgs.FingerPrint);
         }
     }

+ 9 - 5
test/Renci.SshNet.IntegrationTests/HostKeyFile.cs

@@ -2,20 +2,24 @@
 {
     public sealed class HostKeyFile
     {
-        public static readonly HostKeyFile Rsa = new HostKeyFile("ssh-rsa", "/etc/ssh/ssh_host_rsa_key", new byte[] { 0x3d, 0x90, 0xd8, 0x0d, 0xd5, 0xe0, 0xb6, 0x13, 0x42, 0x7c, 0x78, 0x1e, 0x19, 0xa3, 0x99, 0x2b });
-        public static readonly HostKeyFile Dsa = new HostKeyFile("ssh-dsa", "/etc/ssh/ssh_host_dsa_key", new byte[] { 0xcc, 0xb4, 0x4c, 0xe1, 0xba, 0x6d, 0x15, 0x79, 0xec, 0xe1, 0x31, 0x9f, 0xc0, 0x4a, 0x07, 0x9d });
-        public static readonly HostKeyFile Ed25519 = new HostKeyFile("ssh-ed25519", "/etc/ssh/ssh_host_ed25519_key", new byte[] { 0xb3, 0xb9, 0xd0, 0x1b, 0x73, 0xc4, 0x60, 0xb4, 0xce, 0xed, 0x06, 0xf8, 0x58, 0x49, 0xa3, 0xda });
-        public const string Ecdsa = "/etc/ssh/ssh_host_ecdsa_key";
+        public static readonly HostKeyFile Rsa = new HostKeyFile("ssh-rsa", "/etc/ssh/ssh_host_rsa_key", 3072, new byte[] { 0x3d, 0x90, 0xd8, 0x0d, 0xd5, 0xe0, 0xb6, 0x13, 0x42, 0x7c, 0x78, 0x1e, 0x19, 0xa3, 0x99, 0x2b });
+        public static readonly HostKeyFile Dsa = new HostKeyFile("ssh-dsa", "/etc/ssh/ssh_host_dsa_key", 1024, new byte[] { 0xcc, 0xb4, 0x4c, 0xe1, 0xba, 0x6d, 0x15, 0x79, 0xec, 0xe1, 0x31, 0x9f, 0xc0, 0x4a, 0x07, 0x9d });
+        public static readonly HostKeyFile Ed25519 = new HostKeyFile("ssh-ed25519", "/etc/ssh/ssh_host_ed25519_key", 256, new byte[] { 0xb3, 0xb9, 0xd0, 0x1b, 0x73, 0xc4, 0x60, 0xb4, 0xce, 0xed, 0x06, 0xf8, 0x58, 0x49, 0xa3, 0xda });
+        public static readonly HostKeyFile Ecdsa256 = new HostKeyFile("ecdsa-sha2-nistp256", "/etc/ssh/ssh_host_ecdsa256_key", 256, new byte[] { 0xbe, 0x98, 0xa1, 0x54, 0x91, 0x2c, 0x96, 0xc3, 0x77, 0x39, 0x6e, 0x37, 0x8e, 0x64, 0x26, 0x72 });
+        public static readonly HostKeyFile Ecdsa384 = new HostKeyFile("ecdsa-sha2-nistp384", "/etc/ssh/ssh_host_ecdsa384_key", 384, new byte[] { 0xab, 0xbb, 0x20, 0x07, 0x3c, 0xb2, 0x89, 0x9e, 0x40, 0xfe, 0x32, 0x56, 0xfe, 0xd9, 0x95, 0x0b });
+        public static readonly HostKeyFile Ecdsa521 = new HostKeyFile("ecdsa-sha2-nistp521", "/etc/ssh/ssh_host_ecdsa521_key", 521, new byte[] { 0x31, 0xed, 0x9c, 0x89, 0x6f, 0xa3, 0xe4, 0x0d, 0x68, 0x6a, 0xe6, 0xde, 0x89, 0x39, 0x08, 0x7d });
 
-        private HostKeyFile(string keyName, string filePath, byte[] fingerPrint)
+        private HostKeyFile(string keyName, string filePath, int keyLength, byte[] fingerPrint)
         {
             KeyName = keyName;
             FilePath = filePath;
+            KeyLength = keyLength;
             FingerPrint = fingerPrint;
         }
 
         public string KeyName { get; }
         public string FilePath { get; }
+        public int KeyLength { get; }
         public byte[] FingerPrint { get; }
     }
 

+ 0 - 0
test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa_key → test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa256_key


+ 10 - 0
test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa384_key

@@ -0,0 +1,10 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
+1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTgDkIT8et55gvBkZtajzYwOVB/4Ld+
+od3kezy9w/FdGQSk/jYU9XwdHgK1c3Zd9jU8gTEWk74i+7hVhgj5/pAauHvPUBMaPwbbV2
+gUdxUlUz309/OK0wnHDoPGQtfQaUEAAADYCpCICAqQiAgAAAATZWNkc2Etc2hhMi1uaXN0
+cDM4NAAAAAhuaXN0cDM4NAAAAGEE4A5CE/HreeYLwZGbWo82MDlQf+C3fqHd5Hs8vcPxXR
+kEpP42FPV8HR4CtXN2XfY1PIExFpO+Ivu4VYYI+f6QGrh7z1ATGj8G21doFHcVJVM99Pfz
+itMJxw6DxkLX0GlBAAAAMQDjvuyN+AnmAiQe82BX5rnXvKQ3GjmwFrV23BsCZk0QL+g9hb
+Kv7uTs7RW1P66qY3sAAAAOcm9iZXJ0QFJIRTE0RzMB
+-----END OPENSSH PRIVATE KEY-----

+ 12 - 0
test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa521_key

@@ -0,0 +1,12 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
+1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBKwQaI3rV6EjiaB+9ULzsgFMmhDfZ
+EClM6LkdFdsgpGaQXUvk2C3FXbg5F1NHb64+p6m0t9iKxusdzw5PweJHbiQBdKvNHYKAhz
+1hObd28CbxgGYdAARBBE3lf6hbUyGfxwyDXFxX4R+s1lSwsOYPovWDqsduMcGW0Ec1bG7s
+BEQnvu0AAAEQyfIPKMnyDygAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
+AAAIUEASsEGiN61ehI4mgfvVC87IBTJoQ32RApTOi5HRXbIKRmkF1L5NgtxV24ORdTR2+u
+PqeptLfYisbrHc8OT8HiR24kAXSrzR2CgIc9YTm3dvAm8YBmHQAEQQRN5X+oW1Mhn8cMg1
+xcV+EfrNZUsLDmD6L1g6rHbjHBltBHNWxu7AREJ77tAAAAQXzxGU4UtRC1ZpAyFhMkUENw
+M/ZfyVA1VPMjR6BfEPnt6hDN4ZqdhZ5oHr9306OSFAt9pboNqEiv12uPc2gUnD5iAAAADn
+JvYmVydEBSSEUxNEczAQIDBAU=
+-----END OPENSSH PRIVATE KEY-----