Просмотр исходного кода

Ref System.Formats.Asn1 nuget package (#1490)

* Ref System.Formats.Asn1 nuget package

* Ref System.Formats.Asn1 only for net462, netstandard2.0 and netstandard2.1
Delete ObjectIdentifier.cs

* Consolidate package reference

* 100x improvement

---------

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
Scott Xu 1 год назад
Родитель
Сommit
f4bf62b847

+ 1 - 1
Directory.Packages.props

@@ -20,7 +20,7 @@
     <PackageVersion Include="Nerdbank.GitVersioning" Version="3.7.70-alpha" />
     <PackageVersion Include="SonarAnalyzer.CSharp" Version="9.19.0.84025" />
     <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
-    <PackageVersion Include="System.Memory" Version="4.5.5" />
+    <PackageVersion Include="System.Formats.Asn1" Version="8.0.1" />
     <PackageVersion Include="Testcontainers" Version="3.9.0" />
   </ItemGroup>
 </Project>

+ 0 - 439
src/Renci.SshNet/Common/DerData.cs

@@ -1,439 +0,0 @@
-using System;
-using System.Buffers.Binary;
-using System.Collections.Generic;
-using System.Numerics;
-
-namespace Renci.SshNet.Common
-{
-    /// <summary>
-    /// Base class for DER encoded data.
-    /// </summary>
-    public class DerData
-    {
-        private const byte Constructed = 0x20;
-
-        private const byte Boolean = 0x01;
-        private const byte Integer = 0x02;
-        private const byte BITSTRING = 0x03;
-        private const byte Octetstring = 0x04;
-        private const byte Null = 0x05;
-        private const byte Objectidentifier = 0x06;
-        private const byte Sequence = 0x10;
-
-        private readonly List<byte> _data;
-        private readonly int _lastIndex;
-        private int _readerIndex;
-
-        /// <summary>
-        /// Gets a value indicating whether end of data is reached.
-        /// </summary>
-        /// <value>
-        /// <see langword="true"/> if end of data is reached; otherwise, <see langword="false"/>.
-        /// </value>
-        public bool IsEndOfData
-        {
-            get
-            {
-                return _readerIndex >= _lastIndex;
-            }
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="DerData"/> class.
-        /// </summary>
-        public DerData()
-        {
-            _data = new List<byte>();
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="DerData"/> class.
-        /// </summary>
-        /// <param name="data">DER encoded data.</param>
-        /// <param name="construct">its a construct.</param>
-        public DerData(byte[] data, bool construct = false)
-        {
-            _data = new List<byte>(data);
-            if (construct)
-            {
-                _lastIndex = _readerIndex + data.Length;
-            }
-            else
-            {
-                _ = ReadByte(); // skip dataType
-                var length = ReadLength();
-                _lastIndex = _readerIndex + length;
-            }
-        }
-
-        /// <summary>
-        /// Encodes written data as DER byte array.
-        /// </summary>
-        /// <returns>DER Encoded array.</returns>
-        public byte[] Encode()
-        {
-            var length = _data.Count;
-            var lengthBytes = GetLength(length);
-
-            _data.InsertRange(0, lengthBytes);
-            _data.Insert(0, Constructed | Sequence);
-
-            return _data.ToArray();
-        }
-
-        /// <summary>
-        /// Reads next mpint data type from internal buffer.
-        /// </summary>
-        /// <returns>mpint read.</returns>
-        public BigInteger ReadBigInteger()
-        {
-            var type = ReadByte();
-            if (type != Integer)
-            {
-                throw new InvalidOperationException(string.Format("Invalid data type, INTEGER(02) is expected, but was {0}", type.ToString("X2")));
-            }
-
-            var length = ReadLength();
-
-            var data = ReadBytes(length);
-
-#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
-            return new BigInteger(data, isBigEndian: true);
-#else
-            return new BigInteger(data.Reverse());
-#endif
-        }
-
-        /// <summary>
-        /// Reads next int data type from internal buffer.
-        /// </summary>
-        /// <returns>int read.</returns>
-        public int ReadInteger()
-        {
-            var type = ReadByte();
-            if (type != Integer)
-            {
-                throw new InvalidOperationException(string.Format("Invalid data type, INTEGER(02) is expected, but was {0}", type.ToString("X2")));
-            }
-
-            var length = ReadLength();
-
-            var data = ReadBytes(length);
-
-            if (length > 4)
-            {
-                throw new InvalidOperationException("Integer type cannot occupy more then 4 bytes");
-            }
-
-            var result = 0;
-            var shift = (length - 1) * 8;
-            for (var i = 0; i < length; i++)
-            {
-                result |= data[i] << shift;
-                shift -= 8;
-            }
-
-            return result;
-        }
-
-        /// <summary>
-        /// Reads next octetstring data type from internal buffer.
-        /// </summary>
-        /// <returns>data read.</returns>
-        public byte[] ReadOctetString()
-        {
-            var type = ReadByte();
-            if (type != Octetstring)
-            {
-                throw new InvalidOperationException(string.Format("Invalid data type, OCTETSTRING(04) is expected, but was {0}", type.ToString("X2")));
-            }
-
-            var length = ReadLength();
-            var data = ReadBytes(length);
-            return data;
-        }
-
-        /// <summary>
-        /// Reads next bitstring data type from internal buffer.
-        /// </summary>
-        /// <returns>data read.</returns>
-        public byte[] ReadBitString()
-        {
-            var type = ReadByte();
-            if (type != BITSTRING)
-            {
-                throw new InvalidOperationException(string.Format("Invalid data type, BITSTRING(03) is expected, but was {0}", type.ToString("X2")));
-            }
-
-            var length = ReadLength();
-            var data = ReadBytes(length);
-            return data;
-        }
-
-        /// <summary>
-        /// Reads next object data type from internal buffer.
-        /// </summary>
-        /// <returns>data read.</returns>
-        public byte[] ReadObject()
-        {
-            var type = ReadByte();
-            if (type != Objectidentifier)
-            {
-                throw new InvalidOperationException(string.Format("Invalid data type, OBJECT(06) is expected, but was {0}", type.ToString("X2")));
-            }
-
-            var length = ReadLength();
-            var data = ReadBytes(length);
-            return data;
-        }
-
-        /// <summary>
-        /// Writes BOOLEAN data into internal buffer.
-        /// </summary>
-        /// <param name="data">UInt32 data to write.</param>
-        public void Write(bool data)
-        {
-            _data.Add(Boolean);
-            _data.Add(1);
-            _data.Add((byte)(data ? 1 : 0));
-        }
-
-        /// <summary>
-        /// Writes UInt32 data into internal buffer.
-        /// </summary>
-        /// <param name="data">UInt32 data to write.</param>
-        public void Write(uint data)
-        {
-            var bytes = new byte[sizeof(uint)];
-            BinaryPrimitives.WriteUInt32BigEndian(bytes, data);
-            _data.Add(Integer);
-            var length = GetLength(bytes.Length);
-            WriteBytes(length);
-            WriteBytes(bytes);
-        }
-
-        /// <summary>
-        /// Writes INTEGER data into internal buffer.
-        /// </summary>
-        /// <param name="data">BigInteger data to write.</param>
-        public void Write(BigInteger data)
-        {
-            var bytes = data.ToByteArray(isBigEndian: true);
-            _data.Add(Integer);
-            var length = GetLength(bytes.Length);
-            WriteBytes(length);
-            WriteBytes(bytes);
-        }
-
-        /// <summary>
-        /// Writes OCTETSTRING data into internal buffer.
-        /// </summary>
-        /// <param name="data">The data.</param>
-        public void Write(byte[] data)
-        {
-            _data.Add(Octetstring);
-            var length = GetLength(data.Length);
-            WriteBytes(length);
-            WriteBytes(data);
-        }
-
-        /// <summary>
-        /// Writes OBJECTIDENTIFIER data into internal buffer.
-        /// </summary>
-        /// <param name="identifier">The identifier.</param>
-        public void Write(ObjectIdentifier identifier)
-        {
-            var temp = new ulong[identifier.Identifiers.Length - 1];
-            temp[0] = (identifier.Identifiers[0] * 40) + identifier.Identifiers[1];
-            Buffer.BlockCopy(identifier.Identifiers, 2 * sizeof(ulong), temp, 1 * sizeof(ulong), (identifier.Identifiers.Length - 2) * sizeof(ulong));
-            var bytes = new List<byte>();
-            foreach (var subidentifier in temp)
-            {
-                var item = subidentifier;
-                var buffer = new byte[8];
-                var bufferIndex = buffer.Length - 1;
-
-                var current = (byte)(item & 0x7F);
-                do
-                {
-                    buffer[bufferIndex] = current;
-                    if (bufferIndex < buffer.Length - 1)
-                    {
-                        buffer[bufferIndex] |= 0x80;
-                    }
-
-                    item >>= 7;
-                    current = (byte)(item & 0x7F);
-                    bufferIndex--;
-                }
-                while (current > 0);
-
-                for (var i = bufferIndex + 1; i < buffer.Length; i++)
-                {
-                    bytes.Add(buffer[i]);
-                }
-            }
-
-            _data.Add(Objectidentifier);
-            var length = GetLength(bytes.Count);
-            WriteBytes(length);
-            WriteBytes(bytes);
-        }
-
-        /// <summary>
-        /// Writes DerData data into internal buffer.
-        /// </summary>
-        /// <param name="data">DerData data to write.</param>
-        public void Write(DerData data)
-        {
-            var bytes = data.Encode();
-            _data.AddRange(bytes);
-        }
-
-        /// <summary>
-        /// Writes BITSTRING data into internal buffer.
-        /// </summary>
-        /// <param name="data">The data.</param>
-        public void WriteBitstring(byte[] data)
-        {
-            _data.Add(BITSTRING);
-            var length = GetLength(data.Length);
-            WriteBytes(length);
-            WriteBytes(data);
-        }
-
-        /// <summary>
-        /// Writes OBJECTIDENTIFIER data into internal buffer.
-        /// </summary>
-        /// <param name="bytes">The bytes.</param>
-        public void WriteObjectIdentifier(byte[] bytes)
-        {
-            _data.Add(Objectidentifier);
-            var length = GetLength(bytes.Length);
-            WriteBytes(length);
-            WriteBytes(bytes);
-        }
-
-        /// <summary>
-        /// Writes NULL data into internal buffer.
-        /// </summary>
-        public void WriteNull()
-        {
-            _data.Add(Null);
-            _data.Add(0);
-        }
-
-        private static byte[] GetLength(int length)
-        {
-            if (length > 127)
-            {
-                var size = 1;
-                var val = length;
-
-                while ((val >>= 8) != 0)
-                {
-                    size++;
-                }
-
-                var data = new byte[size];
-                data[0] = (byte)(size | 0x80);
-
-                for (int i = (size - 1) * 8, j = 1; i >= 0; i -= 8, j++)
-                {
-                    data[j] = (byte)(length >> i);
-                }
-
-                return data;
-            }
-
-            return new[] { (byte)length };
-        }
-
-        /// <summary>
-        /// Gets Data Length.
-        /// </summary>
-        /// <returns>
-        /// The length.
-        /// </returns>
-        public int ReadLength()
-        {
-            int length = ReadByte();
-
-            if (length == 0x80)
-            {
-                throw new NotSupportedException("Indefinite-length encoding is not supported.");
-            }
-
-            if (length > 127)
-            {
-                var size = length & 0x7f;
-
-                // Note: The invalid long form "0xff" (see X.690 8.1.3.5c) will be caught here
-                if (size > 4)
-                {
-                    throw new InvalidOperationException(string.Format("DER length is '{0}' and cannot be more than 4 bytes.", size));
-                }
-
-                length = 0;
-                for (var i = 0; i < size; i++)
-                {
-                    int next = ReadByte();
-
-                    length = (length << 8) + next;
-                }
-
-                if (length < 0)
-                {
-                    throw new InvalidOperationException("Corrupted data - negative length found");
-                }
-            }
-
-            return length;
-        }
-
-        /// <summary>
-        /// Write Byte data into internal buffer.
-        /// </summary>
-        /// <param name="data">The data to write.</param>
-        public void WriteBytes(IEnumerable<byte> data)
-        {
-            _data.AddRange(data);
-        }
-
-        /// <summary>
-        /// Reads Byte data into internal buffer.
-        /// </summary>
-        /// <returns>
-        /// The data read.
-        /// </returns>
-        public byte ReadByte()
-        {
-            if (_readerIndex > _data.Count)
-            {
-                throw new InvalidOperationException("Read out of boundaries.");
-            }
-
-            return _data[_readerIndex++];
-        }
-
-        /// <summary>
-        /// Reads lengths Bytes data into internal buffer.
-        /// </summary>
-        /// <returns>
-        /// The data read.
-        /// </returns>
-        /// <param name="length">amount of data to read.</param>
-        public byte[] ReadBytes(int length)
-        {
-            if (_readerIndex + length > _data.Count)
-            {
-                throw new InvalidOperationException("Read out of boundaries.");
-            }
-
-            var result = new byte[length];
-            _data.CopyTo(_readerIndex, result, 0, length);
-            _readerIndex += length;
-            return result;
-        }
-    }
-}

+ 0 - 54
src/Renci.SshNet/Common/ObjectIdentifier.cs

@@ -1,54 +0,0 @@
-using System;
-using System.Linq;
-using System.Security.Cryptography;
-
-namespace Renci.SshNet.Common
-{
-    /// <summary>
-    /// Describes object identifier for DER encoding.
-    /// </summary>
-#pragma warning disable CA1815 // Override equals and operator equals on value types
-    public struct ObjectIdentifier
-#pragma warning restore CA1815 // Override equals and operator equals on value types
-    {
-        /// <summary>
-        /// Gets the object identifier.
-        /// </summary>
-        public ulong[] Identifiers { get; private set; }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ObjectIdentifier"/> struct.
-        /// </summary>
-        /// <param name="identifiers">The identifiers.</param>
-        /// <exception cref="ArgumentNullException"><paramref name="identifiers"/> is <see langword="null"/>.</exception>
-        /// <exception cref="ArgumentException"><paramref name="identifiers"/> has less than two elements.</exception>
-        public ObjectIdentifier(params ulong[] identifiers)
-        {
-            if (identifiers is null)
-            {
-                throw new ArgumentNullException(nameof(identifiers));
-            }
-
-            if (identifiers.Length < 2)
-            {
-                throw new ArgumentException("Must contain at least two elements.", nameof(identifiers));
-            }
-
-            Identifiers = identifiers;
-        }
-
-        internal static ObjectIdentifier FromHashAlgorithmName(HashAlgorithmName hashAlgorithmName)
-        {
-            var oid = CryptoConfig.MapNameToOID(hashAlgorithmName.Name);
-
-            if (oid is null)
-            {
-                throw new ArgumentException($"Could not map `{hashAlgorithmName}` to OID.", nameof(hashAlgorithmName));
-            }
-
-            var identifiers = oid.Split('.').Select(ulong.Parse).ToArray();
-
-            return new ObjectIdentifier(identifiers);
-        }
-    }
-}

+ 5 - 4
src/Renci.SshNet/Renci.SshNet.csproj

@@ -33,18 +33,19 @@
   <PropertyGroup Condition=" $([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0')) ">
     <IsAotCompatible>true</IsAotCompatible>
   </PropertyGroup>
-  
+
   <ItemGroup>
+    <PackageReference Include="BouncyCastle.Cryptography" />
     <PackageReference Include="Nerdbank.GitVersioning" PrivateAssets="all" />
   </ItemGroup>
 
   <ItemGroup Condition=" '$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0' ">
     <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
-    <PackageReference Include="System.Memory" />
+    <PackageReference Include="System.Formats.Asn1" />
   </ItemGroup>
 
-  <ItemGroup>
-    <PackageReference Include="BouncyCastle.Cryptography" />
+  <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1'">
+    <PackageReference Include="System.Formats.Asn1" />
   </ItemGroup>
 
   <ItemGroup>

+ 9 - 11
src/Renci.SshNet/Security/Cryptography/DsaKey.cs

@@ -1,5 +1,6 @@
 #nullable enable
 using System;
+using System.Formats.Asn1;
 using System.Numerics;
 using System.Security.Cryptography;
 
@@ -118,19 +119,16 @@ namespace Renci.SshNet.Security
                 throw new ArgumentNullException(nameof(privateKeyData));
             }
 
-            var der = new DerData(privateKeyData);
-            _ = der.ReadBigInteger(); // skip version
+            var der = new AsnReader(privateKeyData, AsnEncodingRules.DER).ReadSequence();
+            _ = der.ReadInteger(); // skip version
 
-            P = der.ReadBigInteger();
-            Q = der.ReadBigInteger();
-            G = der.ReadBigInteger();
-            Y = der.ReadBigInteger();
-            X = der.ReadBigInteger();
+            P = der.ReadInteger();
+            Q = der.ReadInteger();
+            G = der.ReadInteger();
+            Y = der.ReadInteger();
+            X = der.ReadInteger();
 
-            if (!der.IsEndOfData)
-            {
-                throw new InvalidOperationException("Invalid private key (expected EOF).");
-            }
+            der.ThrowIfNotEmpty();
 
             DSA = LoadDSA();
         }

+ 9 - 68
src/Renci.SshNet/Security/Cryptography/EcdsaKey.cs

@@ -1,6 +1,7 @@
 #nullable enable
 using System;
 using System.Diagnostics;
+using System.Formats.Asn1;
 using System.Numerics;
 using System.Security.Cryptography;
 using System.Text;
@@ -220,51 +221,20 @@ namespace Renci.SshNet.Security
         /// <param name="data">DER encoded private key data.</param>
         public EcdsaKey(byte[] data)
         {
-            var der = new DerData(data);
-            _ = der.ReadBigInteger(); // skip version
+            var der = new AsnReader(data, AsnEncodingRules.DER).ReadSequence();
+            _ = der.ReadInteger(); // skip version
 
-            // PrivateKey
             var privatekey = der.ReadOctetString().TrimLeadingZeros();
 
-            // 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));
-            }
-
-            var construct = der.ReadBytes(der.ReadLength()); // object length
-
-            // curve OID
-            var curve_der = new DerData(construct, construct: true);
-            var curve = curve_der.ReadObject();
-
-            // Construct
-            s0 = der.ReadByte();
-            if ((s0 & 0xe0) != 0xa0)
-            {
-                throw new SshException(string.Format("UnexpectedDER: wanted constructed tag (0xa0-0xbf), got: {0:X}", s0));
-            }
-
-            tag = s0 & 0x1f;
-            if (tag != 1)
-            {
-                throw new SshException(string.Format("expected tag 1 in DER privkey, got: {0}", tag));
-            }
+            var s0 = der.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true));
+            var curve = s0.ReadObjectIdentifier();
 
-            construct = der.ReadBytes(der.ReadLength()); // object length
+            var s1 = der.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true));
+            var pubkey = s1.ReadBitString(out _);
 
-            // PublicKey
-            var pubkey_der = new DerData(construct, construct: true);
-            var pubkey = pubkey_der.ReadBitString().TrimLeadingZeros();
+            der.ThrowIfNotEmpty();
 
-            _impl = Import(OidByteArrayToString(curve), pubkey, privatekey);
+            _impl = Import(curve, pubkey, privatekey);
         }
 
 #pragma warning disable CA1859 // Use concrete types when possible for improved performance
@@ -319,35 +289,6 @@ namespace Renci.SshNet.Security
             throw new SshException("Unexpected Curve Name: " + curve_s);
         }
 
-        private static string OidByteArrayToString(byte[] oid)
-        {
-            var retVal = new StringBuilder();
-
-            for (var i = 0; i < oid.Length; i++)
-            {
-                if (i == 0)
-                {
-                    var b = oid[0] % 40;
-                    var a = (oid[0] - b) / 40;
-                    _ = retVal.AppendFormat("{0}.{1}", a, b);
-                }
-                else
-                {
-                    if (oid[i] < 128)
-                    {
-                        _ = retVal.AppendFormat(".{0}", oid[i]);
-                    }
-                    else
-                    {
-                        _ = retVal.AppendFormat(".{0}", ((oid[i] - 128) * 128) + oid[i + 1]);
-                        i++;
-                    }
-                }
-            }
-
-            return retVal.ToString();
-        }
-
         /// <summary>
         /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
         /// </summary>

+ 12 - 14
src/Renci.SshNet/Security/Cryptography/RsaKey.cs

@@ -1,5 +1,6 @@
 #nullable enable
 using System;
+using System.Formats.Asn1;
 using System.Numerics;
 using System.Security.Cryptography;
 
@@ -166,22 +167,19 @@ namespace Renci.SshNet.Security
                 throw new ArgumentNullException(nameof(privateKeyData));
             }
 
-            var der = new DerData(privateKeyData);
-            _ = der.ReadBigInteger(); // skip version
+            var der = new AsnReader(privateKeyData, AsnEncodingRules.DER).ReadSequence();
+            _ = der.ReadInteger(); // skip version
 
-            Modulus = der.ReadBigInteger();
-            Exponent = der.ReadBigInteger();
-            D = der.ReadBigInteger();
-            P = der.ReadBigInteger();
-            Q = der.ReadBigInteger();
-            DP = der.ReadBigInteger();
-            DQ = der.ReadBigInteger();
-            InverseQ = der.ReadBigInteger();
+            Modulus = der.ReadInteger();
+            Exponent = der.ReadInteger();
+            D = der.ReadInteger();
+            P = der.ReadInteger();
+            Q = der.ReadInteger();
+            DP = der.ReadInteger();
+            DQ = der.ReadInteger();
+            InverseQ = der.ReadInteger();
 
-            if (!der.IsEndOfData)
-            {
-                throw new InvalidOperationException("Invalid private key (expected EOF).");
-            }
+            der.ThrowIfNotEmpty();
 
             RSA = RSA.Create();
             RSA.ImportParameters(GetRSAParameters());

+ 0 - 38
test/Renci.SshNet.Tests/Classes/Common/ObjectIdentifierTest.cs

@@ -1,38 +0,0 @@
-using System;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Renci.SshNet.Common;
-using Renci.SshNet.Tests.Common;
-
-namespace Renci.SshNet.Tests.Classes.Common
-{
-    [TestClass]
-    public class ObjectIdentifierTest : TestBase
-    {
-        [TestMethod]
-        public void Constructor_IdentifiersIsNull()
-        {
-            const ulong[] identifiers = null;
-
-            var actualException = Assert.ThrowsException<ArgumentNullException>(() => new ObjectIdentifier(identifiers));
-
-            Assert.AreEqual(typeof(ArgumentNullException), actualException.GetType());
-            Assert.IsNull(actualException.InnerException);
-            Assert.AreEqual(nameof(identifiers), actualException.ParamName);
-        }
-
-        [TestMethod]
-        public void Constructor_LengthOfIdentifiersIsLessThanTwo()
-        {
-            var identifiers = new[] { 5UL };
-
-            var actualException = Assert.ThrowsException<ArgumentException>(() => new ObjectIdentifier(identifiers));
-
-            Assert.AreEqual(typeof(ArgumentException), actualException.GetType());
-            Assert.IsNull(actualException.InnerException);
-            ArgumentExceptionAssert.MessageEquals("Must contain at least two elements.", actualException);
-            Assert.AreEqual(nameof(identifiers), actualException.ParamName);
-        }
-    }
-}