From f8439e967ac1853291d74262c4fcc8cbd04145a2 Mon Sep 17 00:00:00 2001 From: Peter Dettman Date: Thu, 18 Jan 2024 14:41:27 +0700 Subject: Overhaul DerObjectIdentifier and Asn1RelativeOid - contents is now primary data - don't generate identifier string during parsing - improved validation - add TryFromID methods --- crypto/src/asn1/Asn1RelativeOid.cs | 227 ++++++++++++++++----------- crypto/src/asn1/DerObjectIdentifier.cs | 165 +++++++++++-------- crypto/test/src/asn1/test/OIDTest.cs | 47 ++++-- crypto/test/src/asn1/test/RelativeOidTest.cs | 4 + 4 files changed, 279 insertions(+), 164 deletions(-) diff --git a/crypto/src/asn1/Asn1RelativeOid.cs b/crypto/src/asn1/Asn1RelativeOid.cs index 1d2ecb3df..f43a85479 100644 --- a/crypto/src/asn1/Asn1RelativeOid.cs +++ b/crypto/src/asn1/Asn1RelativeOid.cs @@ -24,6 +24,9 @@ namespace Org.BouncyCastle.Asn1 public static Asn1RelativeOid FromContents(byte[] contents) { + if (contents == null) + throw new ArgumentNullException(nameof(contents)); + return CreatePrimitive(contents, true); } @@ -61,10 +64,24 @@ namespace Org.BouncyCastle.Asn1 return (Asn1RelativeOid)Meta.Instance.GetContextInstance(taggedObject, declaredExplicit); } + public static bool TryFromID(string identifier, out Asn1RelativeOid oid) + { + if (identifier == null) + throw new ArgumentNullException(nameof(identifier)); + if (!IsValidIdentifier(identifier, 0)) + { + oid = default; + return false; + } + + oid = new Asn1RelativeOid(ParseIdentifier(identifier), identifier); + return true; + } + private const long LongLimit = (long.MaxValue >> 7) - 0x7F; - private readonly string identifier; - private byte[] contents; + private readonly byte[] m_contents; + private string m_identifier; public Asn1RelativeOid(string identifier) { @@ -73,7 +90,8 @@ namespace Org.BouncyCastle.Asn1 if (!IsValidIdentifier(identifier, 0)) throw new FormatException("string " + identifier + " not a relative OID"); - this.identifier = identifier; + m_contents = ParseIdentifier(identifier); + m_identifier = identifier; } private Asn1RelativeOid(Asn1RelativeOid oid, string branchID) @@ -81,86 +99,92 @@ namespace Org.BouncyCastle.Asn1 if (!IsValidIdentifier(branchID, 0)) throw new FormatException("string " + branchID + " not a valid relative OID branch"); - this.identifier = oid.Id + "." + branchID; + m_contents = Arrays.Concatenate(oid.m_contents, ParseIdentifier(branchID)); + m_identifier = oid.GetID() + "." + branchID; } private Asn1RelativeOid(byte[] contents, bool clone) { - this.identifier = ParseContents(contents); - this.contents = clone ? Arrays.Clone(contents) : contents; + if (!IsValidContents(contents)) + throw new ArgumentException("invalid relative OID contents", nameof(contents)); + + m_contents = clone ? Arrays.Clone(contents) : contents; + m_identifier = null; } - public virtual Asn1RelativeOid Branch(string branchID) + private Asn1RelativeOid(byte[] contents, string identifier) { - return new Asn1RelativeOid(this, branchID); + m_contents = contents; + m_identifier = identifier; } - public string Id + public virtual Asn1RelativeOid Branch(string branchID) { - get { return identifier; } + return new Asn1RelativeOid(this, branchID); } - public override string ToString() + public string GetID() { - return identifier; + return Objects.EnsureSingletonInitialized(ref m_identifier, m_contents, ParseContents); } + // TODO[api] + //[Obsolete("Use 'GetID' instead")] + public string Id => GetID(); + + public override string ToString() => GetID(); + protected override bool Asn1Equals(Asn1Object asn1Object) { - Asn1RelativeOid that = asn1Object as Asn1RelativeOid; - return null != that - && this.identifier == that.identifier; + return asn1Object is Asn1RelativeOid that + && Arrays.AreEqual(this.m_contents, that.m_contents); } protected override int Asn1GetHashCode() { - return identifier.GetHashCode(); + return Arrays.GetHashCode(m_contents); } internal override IAsn1Encoding GetEncoding(int encoding) { - return new PrimitiveEncoding(Asn1Tags.Universal, Asn1Tags.RelativeOid, GetContents()); + return new PrimitiveEncoding(Asn1Tags.Universal, Asn1Tags.RelativeOid, m_contents); } internal override IAsn1Encoding GetEncodingImplicit(int encoding, int tagClass, int tagNo) { - return new PrimitiveEncoding(tagClass, tagNo, GetContents()); + return new PrimitiveEncoding(tagClass, tagNo, m_contents); } internal sealed override DerEncoding GetEncodingDer() { - return new PrimitiveDerEncoding(Asn1Tags.Universal, Asn1Tags.RelativeOid, GetContents()); + return new PrimitiveDerEncoding(Asn1Tags.Universal, Asn1Tags.RelativeOid, m_contents); } internal sealed override DerEncoding GetEncodingDerImplicit(int tagClass, int tagNo) { - return new PrimitiveDerEncoding(tagClass, tagNo, GetContents()); + return new PrimitiveDerEncoding(tagClass, tagNo, m_contents); } - private byte[] GetContents() => Objects.EnsureSingletonInitialized(ref contents, identifier, CreateContents); + internal static Asn1RelativeOid CreatePrimitive(byte[] contents, bool clone) + { + return new Asn1RelativeOid(contents, clone); + } - private static byte[] CreateContents(string identifier) + internal static bool IsValidContents(byte[] contents) { - MemoryStream bOut = new MemoryStream(); - OidTokenizer tok = new OidTokenizer(identifier); - while (tok.HasMoreTokens) + if (contents.Length < 1) + return false; + + bool subIDStart = true; + for (int i = 0; i < contents.Length; ++i) { - string token = tok.NextToken(); - if (token.Length <= 18) - { - WriteField(bOut, long.Parse(token)); - } - else - { - WriteField(bOut, new BigInteger(token)); - } + if (subIDStart && contents[i] == 0x80) + return false; + + subIDStart = (contents[i] & 0x80) == 0; } - return bOut.ToArray(); - } - internal static Asn1RelativeOid CreatePrimitive(byte[] contents, bool clone) - { - return new Asn1RelativeOid(contents, clone); + return subIDStart; } internal static bool IsValidIdentifier(string identifier, int from) @@ -195,59 +219,7 @@ namespace Org.BouncyCastle.Asn1 return true; } - internal static void WriteField(Stream outputStream, long fieldValue) - { -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - Span result = stackalloc byte[9]; -#else - byte[] result = new byte[9]; -#endif - int pos = 8; - result[pos] = (byte)((int)fieldValue & 0x7F); - while (fieldValue >= (1L << 7)) - { - fieldValue >>= 7; - result[--pos] = (byte)((int)fieldValue | 0x80); - } -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - outputStream.Write(result[pos..]); -#else - outputStream.Write(result, pos, 9 - pos); -#endif - } - - internal static void WriteField(Stream outputStream, BigInteger fieldValue) - { - int byteCount = (fieldValue.BitLength + 6) / 7; - if (byteCount == 0) - { - outputStream.WriteByte(0); - } - else - { - BigInteger tmpValue = fieldValue; -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - Span tmp = byteCount <= 16 - ? stackalloc byte[byteCount] - : new byte[byteCount]; -#else - byte[] tmp = new byte[byteCount]; -#endif - for (int i = byteCount - 1; i >= 0; i--) - { - tmp[i] = (byte)(tmpValue.IntValue | 0x80); - tmpValue = tmpValue.ShiftRight(7); - } - tmp[byteCount - 1] &= 0x7F; -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - outputStream.Write(tmp); -#else - outputStream.Write(tmp, 0, tmp.Length); -#endif - } - } - - private static string ParseContents(byte[] contents) + internal static string ParseContents(byte[] contents) { StringBuilder objId = new StringBuilder(); long value = 0; @@ -311,5 +283,76 @@ namespace Org.BouncyCastle.Asn1 return objId.ToString(); } + + internal static byte[] ParseIdentifier(string identifier) + { + MemoryStream bOut = new MemoryStream(); + OidTokenizer tok = new OidTokenizer(identifier); + while (tok.HasMoreTokens) + { + string token = tok.NextToken(); + if (token.Length <= 18) + { + WriteField(bOut, long.Parse(token)); + } + else + { + WriteField(bOut, new BigInteger(token)); + } + } + return bOut.ToArray(); + } + + internal static void WriteField(Stream outputStream, long fieldValue) + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + Span result = stackalloc byte[9]; +#else + byte[] result = new byte[9]; +#endif + int pos = 8; + result[pos] = (byte)((int)fieldValue & 0x7F); + while (fieldValue >= (1L << 7)) + { + fieldValue >>= 7; + result[--pos] = (byte)((int)fieldValue | 0x80); + } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + outputStream.Write(result[pos..]); +#else + outputStream.Write(result, pos, 9 - pos); +#endif + } + + internal static void WriteField(Stream outputStream, BigInteger fieldValue) + { + int byteCount = (fieldValue.BitLength + 6) / 7; + if (byteCount == 0) + { + outputStream.WriteByte(0); + } + else + { + BigInteger tmpValue = fieldValue; +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + Span tmp = byteCount <= 16 + ? stackalloc byte[byteCount] + : new byte[byteCount]; +#else + byte[] tmp = new byte[byteCount]; +#endif + for (int i = byteCount - 1; i >= 0; i--) + { + tmp[i] = (byte)(tmpValue.IntValue | 0x80); + tmpValue = tmpValue.ShiftRight(7); + } + tmp[byteCount - 1] &= 0x7F; +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + outputStream.Write(tmp); +#else + outputStream.Write(tmp, 0, tmp.Length); +#endif + } + } } } diff --git a/crypto/src/asn1/DerObjectIdentifier.cs b/crypto/src/asn1/DerObjectIdentifier.cs index f43724e1f..7d45c9cb8 100644 --- a/crypto/src/asn1/DerObjectIdentifier.cs +++ b/crypto/src/asn1/DerObjectIdentifier.cs @@ -25,6 +25,9 @@ namespace Org.BouncyCastle.Asn1 public static DerObjectIdentifier FromContents(byte[] contents) { + if (contents == null) + throw new ArgumentNullException(nameof(contents)); + return CreatePrimitive(contents, true); } @@ -79,12 +82,26 @@ namespace Org.BouncyCastle.Asn1 return (DerObjectIdentifier)Meta.Instance.GetContextInstance(taggedObject, declaredExplicit); } + public static bool TryFromID(string identifier, out DerObjectIdentifier oid) + { + if (identifier == null) + throw new ArgumentNullException(nameof(identifier)); + if (!IsValidIdentifier(identifier)) + { + oid = default; + return false; + } + + oid = new DerObjectIdentifier(ParseIdentifier(identifier), identifier); + return true; + } + private const long LongLimit = (long.MaxValue >> 7) - 0x7F; private static readonly DerObjectIdentifier[] Cache = new DerObjectIdentifier[1024]; - private readonly string identifier; - private byte[] contents; + private readonly byte[] m_contents; + private string m_identifier; public DerObjectIdentifier(string identifier) { @@ -93,21 +110,32 @@ namespace Org.BouncyCastle.Asn1 if (!IsValidIdentifier(identifier)) throw new FormatException("string " + identifier + " not an OID"); - this.identifier = identifier; + m_contents = ParseIdentifier(identifier); + m_identifier = identifier; } private DerObjectIdentifier(DerObjectIdentifier oid, string branchID) { if (!Asn1RelativeOid.IsValidIdentifier(branchID, 0)) - throw new ArgumentException("string " + branchID + " not a valid OID branch", "branchID"); + throw new FormatException("string " + branchID + " not a valid OID branch"); - this.identifier = oid.Id + "." + branchID; + m_contents = Arrays.Concatenate(oid.m_contents, Asn1RelativeOid.ParseIdentifier(branchID)); + m_identifier = oid.GetID() + "." + branchID; } private DerObjectIdentifier(byte[] contents, bool clone) { - this.identifier = ParseContents(contents); - this.contents = clone ? Arrays.Clone(contents) : contents; + if (!Asn1RelativeOid.IsValidContents(contents)) + throw new ArgumentException("invalid OID contents", nameof(contents)); + + m_contents = clone ? Arrays.Clone(contents) : contents; + m_identifier = null; + } + + private DerObjectIdentifier(byte[] contents, string identifier) + { + m_contents = contents; + m_identifier = identifier; } public virtual DerObjectIdentifier Branch(string branchID) @@ -115,11 +143,15 @@ namespace Org.BouncyCastle.Asn1 return new DerObjectIdentifier(this, branchID); } - public string Id + public string GetID() { - get { return identifier; } + return Objects.EnsureSingletonInitialized(ref m_identifier, m_contents, ParseContents); } + // TODO[api] + //[Obsolete("Use 'GetID' instead")] + public string Id => GetID(); + /** * Return true if this oid is an extension of the passed in branch, stem. * @param stem the arc or branch that is a possible parent. @@ -127,81 +159,44 @@ namespace Org.BouncyCastle.Asn1 */ public virtual bool On(DerObjectIdentifier stem) { - string id = Id, stemId = stem.Id; - return id.Length > stemId.Length && id[stemId.Length] == '.' && Platform.StartsWith(id, stemId); - } + byte[] contents = m_contents, stemContents = stem.m_contents; + int stemLength = stemContents.Length; - public override string ToString() - { - return identifier; + return contents.Length > stemLength + && Arrays.AreEqual(contents, 0, stemLength, stemContents, 0, stemLength); } + public override string ToString() => GetID(); + protected override bool Asn1Equals(Asn1Object asn1Object) { - DerObjectIdentifier that = asn1Object as DerObjectIdentifier; - return null != that - && this.identifier == that.identifier; + return asn1Object is DerObjectIdentifier that + && Arrays.AreEqual(this.m_contents, that.m_contents); } protected override int Asn1GetHashCode() { - return identifier.GetHashCode(); + return Arrays.GetHashCode(m_contents); } internal override IAsn1Encoding GetEncoding(int encoding) { - return new PrimitiveEncoding(Asn1Tags.Universal, Asn1Tags.ObjectIdentifier, GetContents()); + return new PrimitiveEncoding(Asn1Tags.Universal, Asn1Tags.ObjectIdentifier, m_contents); } internal override IAsn1Encoding GetEncodingImplicit(int encoding, int tagClass, int tagNo) { - return new PrimitiveEncoding(tagClass, tagNo, GetContents()); + return new PrimitiveEncoding(tagClass, tagNo, m_contents); } internal sealed override DerEncoding GetEncodingDer() { - return new PrimitiveDerEncoding(Asn1Tags.Universal, Asn1Tags.ObjectIdentifier, GetContents()); + return new PrimitiveDerEncoding(Asn1Tags.Universal, Asn1Tags.ObjectIdentifier, m_contents); } internal sealed override DerEncoding GetEncodingDerImplicit(int tagClass, int tagNo) { - return new PrimitiveDerEncoding(tagClass, tagNo, GetContents()); - } - - private byte[] GetContents() => Objects.EnsureSingletonInitialized(ref contents, identifier, CreateContents); - - private static byte[] CreateContents(string identifier) - { - MemoryStream bOut = new MemoryStream(); - OidTokenizer tok = new OidTokenizer(identifier); - - string token = tok.NextToken(); - int first = int.Parse(token) * 40; - - token = tok.NextToken(); - if (token.Length <= 18) - { - Asn1RelativeOid.WriteField(bOut, first + long.Parse(token)); - } - else - { - Asn1RelativeOid.WriteField(bOut, new BigInteger(token).Add(BigInteger.ValueOf(first))); - } - - while (tok.HasMoreTokens) - { - token = tok.NextToken(); - if (token.Length <= 18) - { - Asn1RelativeOid.WriteField(bOut, long.Parse(token)); - } - else - { - Asn1RelativeOid.WriteField(bOut, new BigInteger(token)); - } - } - - return bOut.ToArray(); + return new PrimitiveDerEncoding(tagClass, tagNo, m_contents); } internal static DerObjectIdentifier CreatePrimitive(byte[] contents, bool clone) @@ -213,7 +208,7 @@ namespace Org.BouncyCastle.Asn1 index &= 1023; var originalEntry = Volatile.Read(ref Cache[index]); - if (originalEntry != null && Arrays.AreEqual(contents, originalEntry.GetContents())) + if (originalEntry != null && Arrays.AreEqual(contents, originalEntry.m_contents)) return originalEntry; var newEntry = new DerObjectIdentifier(contents, clone); @@ -221,7 +216,7 @@ namespace Org.BouncyCastle.Asn1 var exchangedEntry = Interlocked.CompareExchange(ref Cache[index], newEntry, originalEntry); if (exchangedEntry != originalEntry) { - if (exchangedEntry != null && Arrays.AreEqual(contents, exchangedEntry.GetContents())) + if (exchangedEntry != null && Arrays.AreEqual(contents, exchangedEntry.m_contents)) return exchangedEntry; } @@ -237,7 +232,19 @@ namespace Org.BouncyCastle.Asn1 if (first < '0' || first > '2') return false; - return Asn1RelativeOid.IsValidIdentifier(identifier, 2); + if (!Asn1RelativeOid.IsValidIdentifier(identifier, 2)) + return false; + + if (first == '2') + return true; + + if (identifier.Length == 3 || identifier[3] == '.') + return true; + + if (identifier.Length == 4 || identifier[4] == '.') + return identifier[2] < '4'; + + return false; } private static string ParseContents(byte[] contents) @@ -314,5 +321,39 @@ namespace Org.BouncyCastle.Asn1 return objId.ToString(); } + + private static byte[] ParseIdentifier(string identifier) + { + MemoryStream bOut = new MemoryStream(); + OidTokenizer tok = new OidTokenizer(identifier); + + string token = tok.NextToken(); + int first = int.Parse(token) * 40; + + token = tok.NextToken(); + if (token.Length <= 18) + { + Asn1RelativeOid.WriteField(bOut, first + long.Parse(token)); + } + else + { + Asn1RelativeOid.WriteField(bOut, new BigInteger(token).Add(BigInteger.ValueOf(first))); + } + + while (tok.HasMoreTokens) + { + token = tok.NextToken(); + if (token.Length <= 18) + { + Asn1RelativeOid.WriteField(bOut, long.Parse(token)); + } + else + { + Asn1RelativeOid.WriteField(bOut, new BigInteger(token)); + } + } + + return bOut.ToArray(); + } } } diff --git a/crypto/test/src/asn1/test/OIDTest.cs b/crypto/test/src/asn1/test/OIDTest.cs index 6b6993855..5c8142554 100644 --- a/crypto/test/src/asn1/test/OIDTest.cs +++ b/crypto/test/src/asn1/test/OIDTest.cs @@ -44,19 +44,23 @@ namespace Org.BouncyCastle.Asn1.Tests private void CheckValid(string oid) { - DerObjectIdentifier o = new DerObjectIdentifier(oid); + Assert.True(DerObjectIdentifier.TryFromID(oid, out var ignore)); + + DerObjectIdentifier o = new DerObjectIdentifier(oid); o = (DerObjectIdentifier)Asn1Object.FromByteArray(o.GetEncoded()); if (!o.Id.Equals(oid)) { Fail("failed oid check: " + oid); } - } + } - private void CheckInvalid(string oid) + private void CheckInvalid(string oid) { - try - { + Assert.False(DerObjectIdentifier.TryFromID(oid, out var ignore)); + + try + { new DerObjectIdentifier(oid); Fail("failed to catch bad oid: " + oid); } @@ -95,8 +99,20 @@ namespace Org.BouncyCastle.Asn1.Tests CheckValid("1.1.127.32512.8323072.2130706432.545460846592.139637976727552.35747322042253312.9151314442816847872"); CheckValid("1.2.123.12345678901.1.1.1"); CheckValid("2.25.196556539987194312349856245628873852187.1"); - - CheckInvalid("0"); + CheckValid("0.0"); + CheckValid("0.0.1"); + CheckValid("0.39"); + CheckValid("0.39.1"); + CheckValid("1.0"); + CheckValid("1.0.1"); + CheckValid("1.39"); + CheckValid("1.39.1"); + CheckValid("2.0"); + CheckValid("2.0.1"); + CheckValid("2.40"); + CheckValid("2.40.1"); + + CheckInvalid("0"); CheckInvalid("1"); CheckInvalid("2"); CheckInvalid("3.1"); @@ -110,8 +126,16 @@ namespace Org.BouncyCastle.Asn1.Tests CheckInvalid(".12.345.77.234."); CheckInvalid("1.2.3.4.A.5"); CheckInvalid("1,2"); + CheckInvalid("0.40"); + CheckInvalid("0.40.1"); + CheckInvalid("0.100"); + CheckInvalid("0.100.1"); + CheckInvalid("1.40"); + CheckInvalid("1.40.1"); + CheckInvalid("1.100"); + CheckInvalid("1.100.1"); - BranchCheck("1.1", "2.2"); + BranchCheck("1.1", "2.2"); OnCheck("1.1", "1.1", false); OnCheck("1.1", "1.2", false); @@ -121,9 +145,12 @@ namespace Org.BouncyCastle.Asn1.Tests OnCheck("1.12", "1.1.2", false); OnCheck("1.1", "1.1.1", true); OnCheck("1.1", "1.1.2", true); - } + OnCheck("1.2.3.4.5.6", "1.2.3.4.5.6", false); + OnCheck("1.2.3.4.5.6", "1.2.3.4.5.6.7", true); + OnCheck("1.2.3.4.5.6", "1.2.3.4.5.6.7.8", true); + } - [Test] + [Test] public void TestFunction() { string resultText = Perform().ToString(); diff --git a/crypto/test/src/asn1/test/RelativeOidTest.cs b/crypto/test/src/asn1/test/RelativeOidTest.cs index f3d5ba13b..c7d13af68 100644 --- a/crypto/test/src/asn1/test/RelativeOidTest.cs +++ b/crypto/test/src/asn1/test/RelativeOidTest.cs @@ -40,6 +40,8 @@ namespace Org.BouncyCastle.Asn1.Tests private void CheckValid(string oid) { + Assert.True(Asn1RelativeOid.TryFromID(oid, out var ignore)); + Asn1RelativeOid o = new Asn1RelativeOid(oid); o = (Asn1RelativeOid)Asn1Object.FromByteArray(o.GetEncoded()); @@ -51,6 +53,8 @@ namespace Org.BouncyCastle.Asn1.Tests private void CheckInvalid(string oid) { + Assert.False(Asn1RelativeOid.TryFromID(oid, out var ignore)); + try { new Asn1RelativeOid(oid); -- cgit 1.4.1