From a6108d81332894013ee98b64aeba6bce8253e9fe Mon Sep 17 00:00:00 2001 From: Stefan Rinkes Date: Mon, 26 Nov 2018 20:15:25 +0100 Subject: [PATCH] Add Support for ECDSA Host- and Private-Keys Also known as: - ecdsa-sha2-nistp256 - ecdsa-sha2-nistp384 - ecdsa-sha2-nistp521 Basically it translate between SSH-Data and Microsoft Crypto API. --- .../Renci.SshNet.NET35.csproj | 10 +- .../Renci.SshNet.Tests.NET35.csproj | 20 + .../Classes/PrivateKeyFileTest.cs | 66 ++++ .../Data/Key.ECDSA.Encrypted.txt | 8 + src/Renci.SshNet.Tests/Data/Key.ECDSA.txt | 5 + .../Data/Key.ECDSA384.Encrypted.txt | 9 + src/Renci.SshNet.Tests/Data/Key.ECDSA384.txt | 6 + .../Data/Key.ECDSA521.Encrypted.txt | 10 + src/Renci.SshNet.Tests/Data/Key.ECDSA521.txt | 7 + .../Renci.SshNet.Tests.csproj | 8 + src/Renci.SshNet/Common/DerData.cs | 85 ++++- src/Renci.SshNet/ConnectionInfo.cs | 6 +- src/Renci.SshNet/PrivateKeyFile.cs | 10 +- src/Renci.SshNet/Renci.SshNet.csproj | 8 +- .../Cryptography/EcdsaDigitalSignature.cs | 186 +++++++++ .../Security/Cryptography/EcdsaKey.cs | 360 ++++++++++++++++++ 16 files changed, 788 insertions(+), 16 deletions(-) create mode 100644 src/Renci.SshNet.Tests/Data/Key.ECDSA.Encrypted.txt create mode 100644 src/Renci.SshNet.Tests/Data/Key.ECDSA.txt create mode 100644 src/Renci.SshNet.Tests/Data/Key.ECDSA384.Encrypted.txt create mode 100644 src/Renci.SshNet.Tests/Data/Key.ECDSA384.txt create mode 100644 src/Renci.SshNet.Tests/Data/Key.ECDSA521.Encrypted.txt create mode 100644 src/Renci.SshNet.Tests/Data/Key.ECDSA521.txt create mode 100644 src/Renci.SshNet/Security/Cryptography/EcdsaDigitalSignature.cs create mode 100644 src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs diff --git a/src/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj b/src/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj index a6878d187..64ee419f3 100644 --- a/src/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj +++ b/src/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj @@ -18,7 +18,7 @@ full false bin\Debug\ - TRACE;DEBUG;FEATURE_REGEX_COMPILE;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_EAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_SETSOCKETOPTION;FEATURE_SOCKET_POLL;FEATURE_STREAM_APM;FEATURE_DNS_SYNC;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_HMAC_RIPEMD160;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_ENCODING_ASCII + TRACE;DEBUG;FEATURE_REGEX_COMPILE;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_EAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_SETSOCKETOPTION;FEATURE_SOCKET_POLL;FEATURE_STREAM_APM;FEATURE_DNS_SYNC;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_HMAC_RIPEMD160;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_ENCODING_ASCII;FEATURE_ECDSA prompt 4 true @@ -29,7 +29,7 @@ none true bin\Release\ - TRACE;FEATURE_REGEX_COMPILE;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_EAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_SETSOCKETOPTION;FEATURE_SOCKET_POLL;FEATURE_STREAM_APM;FEATURE_DNS_SYNC;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_HMAC_RIPEMD160;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_ENCODING_ASCII + TRACE;FEATURE_REGEX_COMPILE;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_EAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_SETSOCKETOPTION;FEATURE_SOCKET_POLL;FEATURE_STREAM_APM;FEATURE_DNS_SYNC;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_HMAC_RIPEMD160;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_ENCODING_ASCII;FEATURE_ECDSA prompt 4 bin\Release\Renci.SshNet.xml @@ -695,6 +695,12 @@ Security\Cryptography\Key.cs + + Security\Cryptography\EcdsaDigitalSignature.cs + + + Security\Cryptography\EcdsaKey.cs + Security\Cryptography\RsaDigitalSignature.cs diff --git a/src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj b/src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj index a42cf8ec1..893a6fe0c 100644 --- a/src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj +++ b/src/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj @@ -1737,6 +1737,26 @@ Data\Key.SSH2.RSA.txt + + + Data\Key.ECDSA.txt + + + Data\Key.ECDSA384.txt + + + Data\Key.ECDSA521.txt + + + Data\Key.ECDSA.Encrypted.txt + + + Data\Key.ECDSA384.Encrypted.txt + + + Data\Key.ECDSA521.Encrypted.txt + + diff --git a/src/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs b/src/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs index 21bd97853..3c944c8d4 100644 --- a/src/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs +++ b/src/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs @@ -319,6 +319,72 @@ public void Test_PrivateKey_RSA_DES_EDE3_CFB() } } + [TestMethod] + [Owner("darinkes")] + [TestCategory("PrivateKey")] + public void Test_PrivateKey_ECDSA() + { + using (var stream = GetData("Key.ECDSA.txt")) + { + new PrivateKeyFile(stream); + } + } + + [TestMethod] + [Owner("darinkes")] + [TestCategory("PrivateKey")] + public void Test_PrivateKey_ECDSA384() + { + using (var stream = GetData("Key.ECDSA384.txt")) + { + new PrivateKeyFile(stream); + } + } + + [TestMethod] + [Owner("darinkes")] + [TestCategory("PrivateKey")] + public void Test_PrivateKey_ECDSA521() + { + using (var stream = GetData("Key.ECDSA521.txt")) + { + new PrivateKeyFile(stream); + } + } + + [TestMethod] + [Owner("darinkes")] + [TestCategory("PrivateKey")] + public void Test_PrivateKey_ECDSA_Encrypted() + { + using (var stream = GetData("Key.ECDSA.Encrypted.txt")) + { + new PrivateKeyFile(stream, "12345"); + } + } + + [TestMethod] + [Owner("darinkes")] + [TestCategory("PrivateKey")] + public void Test_PrivateKey_ECDSA384_Encrypted() + { + using (var stream = GetData("Key.ECDSA384.Encrypted.txt")) + { + new PrivateKeyFile(stream, "12345"); + } + } + + [TestMethod] + [Owner("darinkes")] + [TestCategory("PrivateKey")] + public void Test_PrivateKey_ECDSA521_Encrypted() + { + using (var stream = GetData("Key.ECDSA521.Encrypted.txt")) + { + new PrivateKeyFile(stream, "12345"); + } + } + /// ///A test for Dispose /// diff --git a/src/Renci.SshNet.Tests/Data/Key.ECDSA.Encrypted.txt b/src/Renci.SshNet.Tests/Data/Key.ECDSA.Encrypted.txt new file mode 100644 index 000000000..f0af5ba7d --- /dev/null +++ b/src/Renci.SshNet.Tests/Data/Key.ECDSA.Encrypted.txt @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,54D46F498C989115AAE14FEA21E3AF11 + +IQdFnndcbzz10d7YQIgEE1TzuzJrm7uYJr4Hvdfz/FshVxMRqxqaqtEgo2vAHHik +BOcPkm+84ERlTNPslcJqLSkKzCdxb7Rz5hfwHuN3Y6Lf01qGakDlzAUEjEyDor+4 +zQtAne+f+gRUJnBvLLoVhH4xdeQFC55GECNUFQpEmos= +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Data/Key.ECDSA.txt b/src/Renci.SshNet.Tests/Data/Key.ECDSA.txt new file mode 100644 index 000000000..13ac9fb49 --- /dev/null +++ b/src/Renci.SshNet.Tests/Data/Key.ECDSA.txt @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEdqaFKgJBIibVjyUh1v7Y35LwIQJrocdTaYFLwl7iB0oAoGCCqGSM49 +AwEHoUQDQgAEQD5MO/n9yqSDTszwzVpApLx5SQFecE5ZfDkgxqVdHQecm1BAPozZ +4eKGNhKn72hT79mLlp9HXX+oNEcuVT83Hw== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Data/Key.ECDSA384.Encrypted.txt b/src/Renci.SshNet.Tests/Data/Key.ECDSA384.Encrypted.txt new file mode 100644 index 000000000..00072ce24 --- /dev/null +++ b/src/Renci.SshNet.Tests/Data/Key.ECDSA384.Encrypted.txt @@ -0,0 +1,9 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,1D64653C5E18C2AACB0B17E3FE43C219 + +lCtRmcvKSeIACwqTtsf/ei1brtCZ386rsk/j7bSXdkZBpvzcmzbeo6w6CYm206Km +hV9TMl2dIO/I1/ov5/2VIR3ZkaElyDOJD/+Be0e3aus4EZj1H1YM/Dv+4QJId+is +Cw4ycWjfudYPPejGdiyjzt5qjaIJwrrEvGtMg7sWVAqDpjcAjS9KuaCu5nOgdItL +s7oHuz+DTGdJQNfUHAlUnz1JaMRWzpP0MwtxdcaRY+w= +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Data/Key.ECDSA384.txt b/src/Renci.SshNet.Tests/Data/Key.ECDSA384.txt new file mode 100644 index 000000000..f2d658ea4 --- /dev/null +++ b/src/Renci.SshNet.Tests/Data/Key.ECDSA384.txt @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCQawHdHLR7NvKa2vPV0sVkbzOE8c0enp95iEysGcGV66RXE1EH//nh +gu5UzeTR4KigBwYFK4EEACKhZANiAAQUk4rVvoOPI1hQzWpNx09Uo6qG+srGcbvB +q15eFK0GnK/T0UBKxdbZ2+//KAYI6SeDHM9t3ORF1aX5EpjTEBI4d7ZY/lV9jX6M +nJ4XuGteJselM2iMmy+p9ZYw83BYB1Y= +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Data/Key.ECDSA521.Encrypted.txt b/src/Renci.SshNet.Tests/Data/Key.ECDSA521.Encrypted.txt new file mode 100644 index 000000000..381b30be8 --- /dev/null +++ b/src/Renci.SshNet.Tests/Data/Key.ECDSA521.Encrypted.txt @@ -0,0 +1,10 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,F995028237EBD79C928530CC6C3E957F + +wT+iajbte4MnpCipVy/7W9t2I8OgwbMjNBw9PB5xmXR1NQX+yWa81DXMTgjHi8++ +6tp+Vlftkr7mY1yvZCVo1Sy4VgcvZeMhtpVKtvYdMCmHJC6gaDOTYX3yee8DJ4FL +fG+IQz0wFyZZ26NFrHiwbufW9z6pXhGNCQZK0KLbFxI9iKwVA0llc7uzTEcmBBpn +0/Snp0CVvX+i6AP9Xj0bBdrFCsvcoT+ZHzS8YWJUfu3m6cpAJksCAy0PXR3ifvus +edTfDpkMxd4/b+DtPB6SMekIAjnQyzbyaTwJCujm8iU= +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Data/Key.ECDSA521.txt b/src/Renci.SshNet.Tests/Data/Key.ECDSA521.txt new file mode 100644 index 000000000..31abe917a --- /dev/null +++ b/src/Renci.SshNet.Tests/Data/Key.ECDSA521.txt @@ -0,0 +1,7 @@ +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIBn2DAme7AU8sCA+/sd6s3c2FNW26IiPvulGd3FC8k5q+fjBZ5LUWR +iJMGrsf2rJLO8hXMGJYoF9tjZEGaabQ8KVagBwYFK4EEACOhgYkDgYYABABrpVjs +ANqcvqMUo1wo0I1uVCXQ6xrauy4iU86FiOwFmkYRrle4w3oYdRJwniC3TwGMuBuM +PMIoCTXr0UtUzn1vkQESNR/J/jAxVseLlVe+KDfZHKvsvk2+O4XaSa1qMfLwN3sp +wlj08+ylKjlO6V3g0hbz4ZaSVwuiRS7Xsv8W2MV6rg== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj index d968dd27b..6f64c0048 100644 --- a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj +++ b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj @@ -709,6 +709,14 @@ Renci.SshNet + + + + + + + + - \ No newline at end of file + diff --git a/src/Renci.SshNet/Security/Cryptography/EcdsaDigitalSignature.cs b/src/Renci.SshNet/Security/Cryptography/EcdsaDigitalSignature.cs new file mode 100644 index 000000000..5b79e6e46 --- /dev/null +++ b/src/Renci.SshNet/Security/Cryptography/EcdsaDigitalSignature.cs @@ -0,0 +1,186 @@ +#if FEATURE_ECDSA +using System; +using Renci.SshNet.Common; +using System.Globalization; + +namespace Renci.SshNet.Security.Cryptography +{ + /// + /// Implements ECDSA digital signature algorithm. + /// + public class EcdsaDigitalSignature : DigitalSignature, IDisposable + { + private readonly EcdsaKey _key; + + /// + /// Initializes a new instance of the class. + /// + /// The ECDSA key. + /// is null. + public EcdsaDigitalSignature(EcdsaKey key) + { + if (key == null) + throw new ArgumentNullException("key"); + + _key = key; + } + + /// + /// Verifies the signature. + /// + /// The input. + /// The signature. + /// + /// true if signature was successfully verified; otherwise false. + /// + public override bool Verify(byte[] input, byte[] signature) + { + // for 521 sig_size is 132 + var sig_size = _key.dsa.KeySize == 521 ? 132 : _key.dsa.KeySize / 4; + var ssh_data = new SshDataSignature(signature, sig_size); + return _key.dsa.VerifyData(input, ssh_data.Signature); + } + + /// + /// Creates the signature. + /// + /// The input. + /// + /// Signed input data. + /// + public override byte[] Sign(byte[] input) + { + var signed = _key.dsa.SignData(input); + var ssh_data = new SshDataSignature(signed.Length); + ssh_data.Signature = signed; + return ssh_data.GetBytes(); + } + + #region IDisposable Members + + private bool _isDisposed; + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) + return; + + if (disposing) + { + _isDisposed = true; + } + } + + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + ~EcdsaDigitalSignature() + { + Dispose(false); + } + + #endregion + } + + class SshDataSignature : SshData + { + private int _signature_size; + + private byte[] _signature_r; + private byte[] _signature_s; + + public byte[] Signature + { + get + { + var signature = new byte[_signature_size]; + Buffer.BlockCopy(_signature_r, 0, signature, 0, _signature_r.Length); + Buffer.BlockCopy(_signature_s, 0, signature, _signature_r.Length, _signature_s.Length); + return signature; + } + set + { + var signed_r = new byte[_signature_size / 2]; + Buffer.BlockCopy(value, 0, signed_r, 0, signed_r.Length); + _signature_r = BigInteger.SshFormatBignum2(signed_r).ToByteArray().Reverse(); + + var signed_s = new byte[_signature_size / 2]; + Buffer.BlockCopy(value, signed_r.Length, signed_s, 0, signed_s.Length); + _signature_s = BigInteger.SshFormatBignum2(signed_s).ToByteArray().Reverse(); + } + } + + public SshDataSignature(int sig_size) + { + _signature_size = sig_size; + } + + public SshDataSignature(byte[] data, int sig_size) + { + _signature_size = sig_size; + Load(data); + } + + protected override void LoadData() + { + _signature_r = Padded(ReadBinary().TrimLeadingZeros(), _signature_size / 2); + _signature_s = Padded(ReadBinary().TrimLeadingZeros(), _signature_size / 2); + } + + protected override void SaveData() + { + WriteBinaryString(BigInteger.SshFormatBignum2(_signature_r).ToByteArray().Reverse()); + WriteBinaryString(BigInteger.SshFormatBignum2(_signature_s).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 + { + var capacity = base.BufferCapacity; + capacity += 4; // r length + capacity += _signature_r.Length; // signature r + capacity += 4; // s length + capacity += _signature_s.Length; // signature s + return capacity; + } + } + + // Fill Data with Leading-Zeros if neccesary + private byte[] Padded(byte[] data, int length) + { + if (length <= data.Length) + return data; + var new_data = new byte[length]; + Buffer.BlockCopy(data, 0, new_data, new_data.Length - data.Length, data.Length); + return new_data; + } + } +} +#endif \ No newline at end of file diff --git a/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs b/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs new file mode 100644 index 000000000..4c1f52dda --- /dev/null +++ b/src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs @@ -0,0 +1,360 @@ +#if FEATURE_ECDSA +using System; +using System.IO; +using System.Text; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using Renci.SshNet.Common; +using Renci.SshNet.Security.Cryptography; + +namespace Renci.SshNet.Security +{ + /// + /// Contains ECDSA (ecdsa-sha2-nistp{256,384,521}) private and public key + /// + public class EcdsaKey : Key, IDisposable + { + internal enum KeyBlobMagicNumber : int + { + 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; + } + + /// + /// Gets dsa + /// + public ECDsaCng dsa; + + private CngKey key; + + /// + /// Gets the SSH name of the ECDSA Key + /// + public override string ToString() + { + return string.Format("ecdsa-sha2-nistp{0}", KeyLength); + } + + /// + /// Gets the length of the key. + /// + /// + /// The length of the key. + /// + public override int KeyLength + { + get + { + return key.KeySize; + } + } + + /// + /// Gets the digital signature. + /// + protected override DigitalSignature DigitalSignature + { + get + { + if (_digitalSignature == null) + { + _digitalSignature = new EcdsaDigitalSignature(this); + } + return _digitalSignature; + } + } + + /// + /// Gets or sets the public. + /// + /// + /// The public. + /// + public override BigInteger[] Public + { + get + { + var blob = key.Export(CngKeyBlobFormat.EccPublicBlob); + + byte[] qx; + byte[] qy; + KeyBlobMagicNumber magic; + using (var br = new BinaryReader(new MemoryStream(blob))) + { + magic = (KeyBlobMagicNumber)br.ReadInt32(); + int cbKey = br.ReadInt32(); + qx = br.ReadBytes(cbKey); + qy = br.ReadBytes(cbKey); + } + + string curve = ""; + switch (magic) + { + case KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P256_MAGIC: + curve = "nistp256"; + break; + case KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P384_MAGIC: + curve = "nistp384"; + break; + case KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P521_MAGIC: + curve = "nistp521"; + break; + default: + throw new SshException("Unexpected Curve Magic: " + magic); + } + var curve_bn = new BigInteger(Encoding.ASCII.GetBytes(curve).Reverse()); + + // Make ECPoint from x and y + // Prepend 04 (uncompressed format) + qx-bytes + qy-bytes + var point_bn_bytes = new byte[1 + qx.Length + qy.Length]; + Buffer.SetByte(point_bn_bytes, 0, 4); + Buffer.BlockCopy(qx, 0, point_bn_bytes, 1, qx.Length); + Buffer.BlockCopy(qy, 0, point_bn_bytes, qx.Length + 1, qy.Length); + + // returns Curve-Name and x/y as ECPoint + return new[] { curve_bn, new BigInteger(point_bn_bytes.Reverse()) }; + } + set + { + // value[0]: + // Curve Name as String + // nistp{256,384,521} + var value_0 = Encoding.ASCII.GetString(value[0].ToByteArray().Reverse()); // Curve Name as String + + var curve_magic = KeyBlobMagicNumber.BCRYPT_ECDH_PRIVATE_GENERIC_MAGIC; + var hash_algo = CngAlgorithm.Sha256; + switch (value_0) + { + case "nistp256": + curve_magic = KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P256_MAGIC; + hash_algo = CngAlgorithm.Sha256; + break; + case "nistp384": + curve_magic = KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P384_MAGIC; + hash_algo = CngAlgorithm.Sha384; + break; + case "nistp521": + curve_magic = KeyBlobMagicNumber.BCRYPT_ECDSA_PUBLIC_P521_MAGIC; + hash_algo = CngAlgorithm.Sha512; + break; + default: + throw new SshException("Unexpected Curve Name: " + value_0); + } + + // ECPoint as BigInteger(2) + var value_1 = value[1].ToByteArray().Reverse(); + + var cord_size = (value_1.Length - 1) / 2; + var value_1_x = new byte[cord_size]; + Buffer.BlockCopy(value_1, 1, value_1_x, 0, value_1_x.Length); // first byte is format. should be checked? + + var value_1_y = new byte[cord_size]; + Buffer.BlockCopy(value_1, cord_size + 1, value_1_y, 0, value_1_y.Length); + + int headerSize = Marshal.SizeOf(typeof(BCRYPT_ECCKEY_BLOB)); + int blobSize = headerSize + (2 * cord_size); + byte[] blob = new byte[blobSize]; + + using (var bw = new BinaryWriter(new MemoryStream(blob))) + { + bw.Write((int)curve_magic); + bw.Write(cord_size); + bw.Write(value_1_x); // q.x + bw.Write(value_1_y); // q.y + } + key = CngKey.Import(blob, CngKeyBlobFormat.EccPublicBlob); + dsa = new ECDsaCng(key) + { + HashAlgorithm = hash_algo + }; + } + } + + private EcdsaDigitalSignature _digitalSignature; + + /// + /// Initializes a new instance of the class. + /// + public EcdsaKey() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// DER encoded private key data. + public EcdsaKey(byte[] data) + { + var der = new DerData(data); + var version = der.ReadBigInteger(); // skip version + + // PrivateKey + var privatekey = der.ReadOctetString(); + + // Construct + var s0 = der.ReadByte(); + if ((s0 & 0xe0) != 0xa0) + throw new SshException(string.Format("UnexpectedDER: wanted constructed tag (0xa0-0xbf), got: {0:X}", s0)); + var tag = s0 & 0x1f; + if (tag != 0) + throw new SshException(string.Format("expected tag 0 in DER privkey, got: {0}", tag)); + der.ReadByte(); // object length + + // curve OID + var curve = der.ReadObject(); + + var pkcs8_data = PemToPkcs8(privatekey, curve, true); + key = ImportPkcs8(pkcs8_data, privatekey); + dsa = new ECDsaCng(key); + + switch (dsa.KeySize) + { + case 256: + dsa.HashAlgorithm = CngAlgorithm.Sha256; + break; + case 384: + dsa.HashAlgorithm = CngAlgorithm.Sha384; + break; + case 521: + dsa.HashAlgorithm = CngAlgorithm.Sha512; + break; + default: + throw new SshException("Unknown KeySize: " + dsa.KeySize); + } + } + + private CngKey ImportPkcs8(byte[] pkcs8_data, byte[] privatekey) + { + // There was an issue in older .NET Versions which prevents + // the usage our keys. Was fixed with .NET 4.6.1. Workaround: + // Change KeyBlobMagicNumber from Key-exchange (ECDH) to Signing (ECDSA) + // See: https://stackoverflow.com/a/43982666 + var key =CngKey.Import(pkcs8_data, CngKeyBlobFormat.Pkcs8PrivateBlob); + + var blob = key.Export(CngKeyBlobFormat.EccPublicBlob); + key.Dispose(); + byte[] qx; + byte[] qy; + KeyBlobMagicNumber magic; + int cbkey; + using (var br = new BinaryReader(new MemoryStream(blob))) + { + magic = (KeyBlobMagicNumber)br.ReadInt32(); + cbkey = br.ReadInt32(); + qx = br.ReadBytes(cbkey); + qy = br.ReadBytes(cbkey); + } + + switch (magic) + { + case KeyBlobMagicNumber.BCRYPT_ECDH_PUBLIC_P256_MAGIC: + magic = KeyBlobMagicNumber.BCRYPT_ECDSA_PRIVATE_P256_MAGIC; + break; + case KeyBlobMagicNumber.BCRYPT_ECDH_PUBLIC_P384_MAGIC: + magic = KeyBlobMagicNumber.BCRYPT_ECDSA_PRIVATE_P384_MAGIC; + break; + case KeyBlobMagicNumber.BCRYPT_ECDH_PUBLIC_P521_MAGIC: + magic = KeyBlobMagicNumber.BCRYPT_ECDSA_PRIVATE_P521_MAGIC; + break; + } + + int headerSize = Marshal.SizeOf(typeof(BCRYPT_ECCKEY_BLOB)); + int blobSize = headerSize + qx.Length + qy.Length + privatekey.Length; + byte[] new_blob = new byte[blobSize]; + + using (var bw = new BinaryWriter(new MemoryStream(new_blob))) + { + bw.Write((int)magic); + bw.Write(cbkey); + bw.Write(qx); // q.x + bw.Write(qy); // q.y + bw.Write(privatekey); // d + } + return CngKey.Import(new_blob, CngKeyBlobFormat.EccPrivateBlob); + } + + // Since Windows Crypto API is unable to parse/read our PEM format directly + // we have to convert it to PKCS#8 Format first + private byte[] PemToPkcs8(byte[] key, byte[] curve, bool isprivate) + { + var newdata = new DerData(); + newdata.Write(new BigInteger(0)); + + // Create Infos about Content and our Curve + var objdata = new DerData(); + // 1.2.840.10045.2.1 - ECDSA and ECDH Public Key. + var obj = new ObjectIdentifier(1, 2, 840, 10045, 2, 1); + objdata.Write(obj); + objdata.WriteObjectIdentifier(curve); + newdata.WriteBytes(objdata.Encode()); + + var keydata = new DerData(); + keydata.Write(new BigInteger(isprivate ? 1: 0)); + keydata.Write(key); + + newdata.Write(keydata.Encode()); + + return newdata.Encode(); + } + #region IDisposable Members + + private bool _isDisposed; + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) + return; + + if (disposing) + { + _isDisposed = true; + } + } + + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + ~EcdsaKey() + { + Dispose(false); + } + + #endregion + } +} +#endif \ No newline at end of file