File: System\Formats\Asn1\AsnDecoder.Oid.cs
Web Access
Project: src\src\libraries\System.Formats.Asn1\src\System.Formats.Asn1.csproj (System.Formats.Asn1)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers.Binary;
using System.Diagnostics;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
 
namespace System.Formats.Asn1
{
    public static partial class AsnDecoder
    {
        /// <summary>
        ///   Reads an Object Identifier value from <paramref name="source"/> with a specified tag under
        ///   the specified encoding rules.
        /// </summary>
        /// <param name="source">The buffer containing encoded data.</param>
        /// <param name="ruleSet">The encoding constraints to use when interpreting the data.</param>
        /// <param name="bytesConsumed">
        ///   When this method returns, the total number of bytes for the encoded value.
        ///   This parameter is treated as uninitialized.
        /// </param>
        /// <param name="expectedTag">
        ///   The tag to check for before reading, or <see langword="null"/> for the default tag (Universal 6).
        /// </param>
        /// <returns>
        ///   The decoded object identifier in the dotted-decimal notation.
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        ///   <paramref name="ruleSet"/> is not defined.
        /// </exception>
        /// <exception cref="AsnContentException">
        ///   The next value does not have the correct tag.
        ///
        ///   -or-
        ///
        ///   The length encoding is not valid under the current encoding rules.
        ///
        ///   -or-
        ///
        ///   The contents are not valid under the current encoding rules.
        /// </exception>
        /// <exception cref="ArgumentException">
        ///   <paramref name="expectedTag"/>.<see cref="Asn1Tag.TagClass"/> is
        ///   <see cref="TagClass.Universal"/>, but
        ///   <paramref name="expectedTag"/>.<see cref="Asn1Tag.TagValue"/> is not correct for
        ///   the method.
        /// </exception>
        public static string ReadObjectIdentifier(
            ReadOnlySpan<byte> source,
            AsnEncodingRules ruleSet,
            out int bytesConsumed,
            Asn1Tag? expectedTag = null)
        {
            // T-REC-X.690-201508 sec 8.19.1
            ReadOnlySpan<byte> contents = GetPrimitiveContentSpan(
                source,
                ruleSet,
                expectedTag ?? Asn1Tag.ObjectIdentifier,
                UniversalTagNumber.ObjectIdentifier,
                out int consumed);
 
#if NET
            string? wellKnown = WellKnownOids.GetValue(contents);
 
            if (wellKnown is not null)
            {
                bytesConsumed = consumed;
                return wellKnown;
            }
#endif
 
            string ret = ReadObjectIdentifier(contents);
            bytesConsumed = consumed;
            return ret;
        }
 
        private static void ReadSubIdentifier(
            ReadOnlySpan<byte> source,
            out int bytesRead,
            out long? smallValue,
            out BigInteger? largeValue)
        {
            Debug.Assert(source.Length > 0);
 
            // T-REC-X.690-201508 sec 8.19.2 (last sentence)
            if (source[0] == 0x80)
            {
                throw new AsnContentException();
            }
 
            // Set semanticBits to a value such that on the first
            // iteration of the loop it becomes the correct value.
            // So each entry here is [real semantic bits for this value] - 7.
            int semanticBits = source[0] switch
            {
                >= 0b1100_0000 => 0,
                >= 0b1010_0000 => -1,
                >= 0b1001_0000 => -2,
                >= 0b1000_1000 => -3,
                >= 0b1000_0100 => -4,
                >= 0b1000_0010 => -5,
                >= 0b1000_0001 => -6,
                _ => 0,
            };
 
            // First, see how long the segment is
            int end = -1;
            int idx;
 
            // None of T-REC-X.660-201107, T-REC-X.680-201508, or T-REC-X.690-201508
            // have any recommendations for a minimum (or maximum) size of a
            // sub-identifier.
            //
            // T-REC-X.667-201210 (and earlier versions) discuss the no-registration-
            // required UUID space at 2.25.{UUID}, where UUIDs are defined as 128-bit
            // values. This gives us a minimum lower bound of 128-bit.
            //
            // Windows Crypt32 has historically only supported 64-bit values, and
            // the "size limitations" FAQ on oid-info.com says that the largest arc
            // value is a 39-digit value that corresponds to a 2.25.UUID value.
            //
            // So, until something argues for a bigger number, our bit-limit is 128.
            const int MaxAllowedBits = 128;
 
            for (idx = 0; idx < source.Length; idx++)
            {
                semanticBits += 7;
 
                if (semanticBits > MaxAllowedBits)
                {
                    throw new AsnContentException(SR.ContentException_OidTooBig);
                }
 
                // If the high bit isn't set this marks the end of the sub-identifier.
                bool endOfIdentifier = (source[idx] & 0x80) == 0;
 
                if (endOfIdentifier)
                {
                    end = idx;
                    break;
                }
            }
 
            if (end < 0)
            {
                throw new AsnContentException();
            }
 
            bytesRead = end + 1;
            long accum = 0;
 
            // Fast path, 9 or fewer bytes => fits in a signed long.
            // (7 semantic bits per byte * 9 bytes = 63 bits, which leaves the sign bit alone)
            if (bytesRead <= 9)
            {
                for (idx = 0; idx < bytesRead; idx++)
                {
                    byte cur = source[idx];
                    accum <<= 7;
                    accum |= (byte)(cur & 0x7F);
                }
 
                largeValue = null;
                smallValue = accum;
                return;
            }
 
            // Slow path, needs temporary storage.
 
            const int SemanticByteCount = 7;
            const int ContentByteCount = 8;
 
            // Every 8 content bytes turns into 7 integer bytes, so scale the count appropriately.
            // Add one while we're shrunk to account for the needed padding byte or the len%8 discarded bytes.
            int bytesRequired = ((bytesRead / ContentByteCount) + 1) * SemanticByteCount;
            byte[] tmpBytes = CryptoPool.Rent(bytesRequired);
            // Ensure all the bytes are zeroed out for BigInteger's parsing.
            Array.Clear(tmpBytes, 0, tmpBytes.Length);
 
            Span<byte> writeSpan = tmpBytes;
            Span<byte> accumValueBytes = stackalloc byte[sizeof(long)];
            int nextStop = bytesRead;
            idx = bytesRead - ContentByteCount;
 
            while (nextStop > 0)
            {
                byte cur = source[idx];
 
                accum <<= 7;
                accum |= (byte)(cur & 0x7F);
 
                idx++;
 
                if (idx >= nextStop)
                {
                    Debug.Assert(idx == nextStop);
                    Debug.Assert(writeSpan.Length >= SemanticByteCount);
 
                    BinaryPrimitives.WriteInt64LittleEndian(accumValueBytes, accum);
                    Debug.Assert(accumValueBytes[7] == 0);
                    accumValueBytes.Slice(0, SemanticByteCount).CopyTo(writeSpan);
                    writeSpan = writeSpan.Slice(SemanticByteCount);
 
                    accum = 0;
                    nextStop -= ContentByteCount;
                    idx = Math.Max(0, nextStop - ContentByteCount);
                }
            }
 
            int bytesWritten = tmpBytes.Length - writeSpan.Length;
 
            // Verify our bytesRequired calculation. There should be at most 7 padding bytes.
            // If the length % 8 is 7 we'll have 0 padding bytes, but the sign bit is still clear.
            //
            // 8 content bytes had a sign bit problem, so we gave it a second 7-byte block, 7 remain.
            // 7 content bytes got a single block but used and wrote 7 bytes, but only 49 of the 56 bits.
            // 6 content bytes have a padding count of 1.
            // 1 content byte has a padding count of 6.
            // 0 content bytes is illegal, but see 8 for the cycle.
            int paddingByteCount = bytesRequired - bytesWritten;
            Debug.Assert(paddingByteCount >= 0 && paddingByteCount < sizeof(long));
 
            largeValue = new BigInteger(tmpBytes);
            smallValue = null;
 
            CryptoPool.Return(tmpBytes, bytesWritten);
        }
 
        private static string ReadObjectIdentifier(ReadOnlySpan<byte> contents)
        {
            // T-REC-X.690-201508 sec 8.19.2 says the minimum length is 1
            if (contents.Length < 1)
            {
                throw new AsnContentException();
            }
 
            // Each byte can contribute a 3 digit value and a '.' (e.g. "126."), but usually
            // they convey one digit and a separator.
            //
            // The OID with the most arcs which were found after a 30 minute search is
            // "1.3.6.1.4.1.311.60.2.1.1" (EV cert jurisdiction of incorporation - locality)
            // which has 11 arcs.
            // The longest "known" segment is 16 bytes, a UUID-as-an-arc value.
            // 16 * 11 = 176 bytes for an "extremely long" OID.
            //
            // So pre-allocate the StringBuilder with at most 1020 characters, an input longer than
            // 255 encoded bytes will just have to re-allocate.
            StringBuilder builder = new StringBuilder(((byte)contents.Length) * 4);
 
            ReadSubIdentifier(contents, out int bytesRead, out long? smallValue, out BigInteger? largeValue);
 
            // T-REC-X.690-201508 sec 8.19.4
            // The first two subidentifiers (X.Y) are encoded as (X * 40) + Y, because Y is
            // bounded [0, 39] for X in {0, 1}, and only X in {0, 1, 2} are legal.
            // So:
            // * identifier < 40 => X = 0, Y = identifier.
            // * identifier < 80 => X = 1, Y = identifier - 40.
            // * else: X = 2, Y = identifier - 80.
            byte firstArc;
 
            if (smallValue != null)
            {
                long firstIdentifier = smallValue.Value;
 
                if (firstIdentifier < 40)
                {
                    firstArc = 0;
                }
                else if (firstIdentifier < 80)
                {
                    firstArc = 1;
                    firstIdentifier -= 40;
                }
                else
                {
                    firstArc = 2;
                    firstIdentifier -= 80;
                }
 
                builder.Append(firstArc);
                builder.Append('.');
                builder.Append(firstIdentifier);
            }
            else
            {
                Debug.Assert(largeValue != null);
                BigInteger firstIdentifier = largeValue.Value;
 
                // We're only here because we were bigger than long.MaxValue, so
                // we're definitely on arc 2.
                Debug.Assert(firstIdentifier > long.MaxValue);
 
                firstArc = 2;
                firstIdentifier -= 80;
 
                builder.Append(firstArc);
                builder.Append('.');
                builder.Append(firstIdentifier.ToString());
            }
 
            contents = contents.Slice(bytesRead);
 
            const int MaxArcs = 64;
            int remainingArcs = MaxArcs - 2;
 
            while (!contents.IsEmpty)
            {
                if (remainingArcs <= 0)
                {
                    throw new AsnContentException(SR.ContentException_OidTooBig);
                }
 
                remainingArcs--;
 
                ReadSubIdentifier(contents, out bytesRead, out smallValue, out largeValue);
                // Exactly one should be non-null.
                Debug.Assert((smallValue == null) != (largeValue == null));
 
                builder.Append('.');
 
                if (smallValue != null)
                {
                    builder.Append(smallValue.Value);
                }
                else
                {
                    builder.Append(largeValue!.Value.ToString());
                }
 
                contents = contents.Slice(bytesRead);
            }
 
            return builder.ToString();
        }
    }
 
    public partial class AsnReader
    {
        /// <summary>
        ///   Reads the next value as an OBJECT IDENTIFIER with a specified tag, returning
        ///   the value in a dotted decimal format string.
        /// </summary>
        /// <param name="expectedTag">
        ///   The tag to check for before reading, or <see langword="null"/> for the default tag (Universal 6).
        /// </param>
        /// <returns>The decoded object identifier in the dotted-decimal notation.</returns>
        /// <exception cref="AsnContentException">
        ///   The next value does not have the correct tag.
        ///
        ///   -or-
        ///
        ///   The length encoding is not valid under the current encoding rules.
        ///
        ///   -or-
        ///
        ///   The contents are not valid under the current encoding rules.
        /// </exception>
        /// <exception cref="ArgumentException">
        ///   <paramref name="expectedTag"/>.<see cref="Asn1Tag.TagClass"/> is
        ///   <see cref="TagClass.Universal"/>, but
        ///   <paramref name="expectedTag"/>.<see cref="Asn1Tag.TagValue"/> is not correct for
        ///   the method.
        /// </exception>
        public string ReadObjectIdentifier(Asn1Tag? expectedTag = null)
        {
            string oidValue =
                AsnDecoder.ReadObjectIdentifier(_data.Span, RuleSet, out int consumed, expectedTag);
 
            _data = _data.Slice(consumed);
            return oidValue;
        }
    }
}