diff --git a/appveyor.yml b/appveyor.yml index ca7144ecb..75efe8966 100644 --- a/appveyor.yml +++ b/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: diff --git a/src/Renci.SshNet/Common/SshDataStream.cs b/src/Renci.SshNet/Common/SshDataStream.cs index c9b7d9c88..a99e4010e 100644 --- a/src/Renci.SshNet/Common/SshDataStream.cs +++ b/src/Renci.SshNet/Common/SshDataStream.cs @@ -56,7 +56,7 @@ public bool IsEndOfData } } -#if NET462 || NETSTANDARD2_0 +#if NETFRAMEWORK || NETSTANDARD2_0 private int Read(Span buffer) { var sharedBuffer = System.Buffers.ArrayPool.Shared.Rent(buffer.Length); diff --git a/src/Renci.SshNet/Security/Cryptography/DsaKey.cs b/src/Renci.SshNet/Security/Cryptography/DsaKey.cs index fa7126184..252dfeee2 100644 --- a/src/Renci.SshNet/Security/Cryptography/DsaKey.cs +++ b/src/Renci.SshNet/Security/Cryptography/DsaKey.cs @@ -41,12 +41,7 @@ public class DsaKey : Key, IDisposable /// public BigInteger X { get; } - /// - /// Gets the length of the key in bits. - /// - /// - /// The bit-length of the key. - /// + /// public override int KeyLength { get diff --git a/src/Renci.SshNet/Security/Cryptography/ED25519Key.cs b/src/Renci.SshNet/Security/Cryptography/ED25519Key.cs index 705924635..b1cfd99cb 100644 --- a/src/Renci.SshNet/Security/Cryptography/ED25519Key.cs +++ b/src/Renci.SshNet/Security/Cryptography/ED25519Key.cs @@ -40,12 +40,7 @@ public override BigInteger[] Public } } - /// - /// Gets the length of the key. - /// - /// - /// The length of the key. - /// + /// public override int KeyLength { get diff --git a/src/Renci.SshNet/Security/Cryptography/EcdsaDigitalSignature.cs b/src/Renci.SshNet/Security/Cryptography/EcdsaDigitalSignature.cs index fd44b41d9..f5e577c06 100644 --- a/src/Renci.SshNet/Security/Cryptography/EcdsaDigitalSignature.cs +++ b/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; /// /// Initializes a new instance of the class. @@ -41,13 +39,8 @@ public override bool Verify(byte[] input, byte[] signature) // 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); } /// @@ -59,13 +52,8 @@ public override bool Verify(byte[] input, byte[] signature) /// 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 @@ public void Dispose() /// to release both managed and unmanaged resources; to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (_isDisposed) - { - return; - } - - if (disposing) - { - _isDisposed = true; - } - } - - /// - /// Finalizes an instance of the class. - /// - ~EcdsaDigitalSignature() - { - Dispose(disposing: false); } private sealed class SshDataSignature : SshData @@ -155,18 +126,6 @@ protected override void SaveData() 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 diff --git a/src/Renci.SshNet/Security/Cryptography/EcdsaKey.BclImpl.cs b/src/Renci.SshNet/Security/Cryptography/EcdsaKey.BclImpl.cs new file mode 100644 index 000000000..76c1701e8 --- /dev/null +++ b/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 diff --git a/src/Renci.SshNet/Security/Cryptography/EcdsaKey.BouncyCastleImpl.cs b/src/Renci.SshNet/Security/Cryptography/EcdsaKey.BouncyCastleImpl.cs new file mode 100644 index 000000000..a6889f893 --- /dev/null +++ b/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 diff --git a/src/Renci.SshNet/Security/Cryptography/EcdsaKey.CngImpl.cs b/src/Renci.SshNet/Security/Cryptography/EcdsaKey.CngImpl.cs new file mode 100644 index 000000000..97f44b56d --- /dev/null +++ b/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 diff --git a/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs b/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs index 6bf244bc4..3307eba8b 100644 --- a/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs +++ b/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 /// /// Contains ECDSA (ecdsa-sha2-nistp{256,384,521}) private and public key. /// - 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 @@ public class EcdsaKey : Key, IDisposable 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 /// /// Gets the SSH name of the ECDSA Key. @@ -68,28 +41,6 @@ public override string ToString() return string.Format("ecdsa-sha2-nistp{0}", KeyLength); } -#if NETFRAMEWORK - /// - /// Gets the HashAlgorithm to use. - /// - 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 /// /// Gets the HashAlgorithm to use. /// @@ -110,25 +61,44 @@ public HashAlgorithmName HashAlgorithm } } } + + 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 - /// - /// Gets the length of the key. - /// - /// - /// The length of the key. - /// + 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); + } + } + + /// public override int KeyLength { get { - return Ecdsa.KeySize; + return _impl.KeyLength; } } - /// - /// Gets the digital signature. - /// + /// protected internal override DigitalSignature DigitalSignature { get @@ -150,88 +120,60 @@ public override BigInteger[] Public { 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()) }; } } /// /// Gets the PrivateKey Bytes. /// - public byte[] PrivateKey { get; private set; } + public byte[]? PrivateKey + { + get + { + return _impl.PrivateKey; + } + } -#if NETFRAMEWORK /// /// Gets the object. /// - public ECDsaCng Ecdsa { get; private set; } +#if NET + public ECDsa Ecdsa #else - /// - /// Gets the object. - /// - public ECDsa Ecdsa { get; private set; } + public ECDsa? Ecdsa #endif + { + get + { + return _impl.Ecdsa; + } + } /// /// Initializes a new instance of the class. @@ -253,7 +195,7 @@ public EcdsaKey(SshKeyData publicKeyData) 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); } /// @@ -264,7 +206,7 @@ public EcdsaKey(SshKeyData publicKeyData) /// Value of privatekey. public EcdsaKey(string curve, byte[] publickey, byte[] privatekey) { - Import(GetCurveOid(curve), publickey, privatekey); + _impl = Import(GetCurveOid(curve), publickey, privatekey); } /// @@ -317,53 +259,13 @@ public EcdsaKey(byte[] data) 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 @@ private void Import(string curve_oid, byte[] publickey, byte[] privatekey) 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(); - 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 @@ public void Dispose() /// to release both managed and unmanaged resources; to release only unmanaged resources. protected virtual void Dispose(bool disposing) { - if (_isDisposed) - { - return; - } - if (disposing) { - _isDisposed = true; + _digitalSignature?.Dispose(); + _impl.Dispose(); } } - - /// - /// Finalizes an instance of the class. - /// - ~EcdsaKey() - { - Dispose(disposing: false); - } } } diff --git a/src/Renci.SshNet/Security/Cryptography/Key.cs b/src/Renci.SshNet/Security/Cryptography/Key.cs index 54ecac7e4..83f6c16c0 100644 --- a/src/Renci.SshNet/Security/Cryptography/Key.cs +++ b/src/Renci.SshNet/Security/Cryptography/Key.cs @@ -22,10 +22,10 @@ public abstract class Key public abstract BigInteger[] Public { get; } /// - /// Gets the length of the key. + /// Gets the length of the key in bits. /// /// - /// The length of the key. + /// The bit-length of the key. /// public abstract int KeyLength { get; } diff --git a/src/Renci.SshNet/Security/Cryptography/RsaKey.cs b/src/Renci.SshNet/Security/Cryptography/RsaKey.cs index 472b097e0..229d448b3 100644 --- a/src/Renci.SshNet/Security/Cryptography/RsaKey.cs +++ b/src/Renci.SshNet/Security/Cryptography/RsaKey.cs @@ -91,12 +91,7 @@ public override string ToString() /// public BigInteger InverseQ { get; } - /// - /// Gets the length of the key in bits. - /// - /// - /// The bit-length of the key. - /// + /// public override int KeyLength { get diff --git a/test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs b/test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs index e685bddf6..ccb153f49 100644 --- a/test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs +++ b/test/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs @@ -26,34 +26,52 @@ public void TearDown() [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 @@ private void DoTest(HostKeyAlgorithm hostKeyAlgorithm, HostKeyFile hostKeyFile, 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); } } diff --git a/test/Renci.SshNet.IntegrationTests/HostKeyFile.cs b/test/Renci.SshNet.IntegrationTests/HostKeyFile.cs index 5562338c2..1ed3e847f 100644 --- a/test/Renci.SshNet.IntegrationTests/HostKeyFile.cs +++ b/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; } } diff --git a/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa_key b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa256_key similarity index 100% rename from test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa_key rename to test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa256_key diff --git a/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa384_key b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa384_key new file mode 100644 index 000000000..dccf7b2fc --- /dev/null +++ b/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----- diff --git a/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa521_key b/test/Renci.SshNet.IntegrationTests/server/ssh/ssh_host_ecdsa521_key new file mode 100644 index 000000000..6675a69c0 --- /dev/null +++ b/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-----