diff --git a/src/libraries/System.Formats.Asn1/ref/System.Formats.Asn1.cs b/src/libraries/System.Formats.Asn1/ref/System.Formats.Asn1.cs index bec9d28b7b426e..f27056100697cc 100644 --- a/src/libraries/System.Formats.Asn1/ref/System.Formats.Asn1.cs +++ b/src/libraries/System.Formats.Asn1/ref/System.Formats.Asn1.cs @@ -55,6 +55,7 @@ public AsnContentException(string? message, System.Exception? inner) { } } public static partial class AsnDecoder { + public static int? DecodeLength(System.ReadOnlySpan source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int bytesConsumed) { throw null; } public static byte[] ReadBitString(System.ReadOnlySpan source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int unusedBitCount, out int bytesConsumed, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; } public static bool ReadBoolean(System.ReadOnlySpan source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int bytesConsumed, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; } public static string ReadCharacterString(System.ReadOnlySpan source, System.Formats.Asn1.AsnEncodingRules ruleSet, System.Formats.Asn1.UniversalTagNumber encodingType, out int bytesConsumed, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; } @@ -74,6 +75,7 @@ public static partial class AsnDecoder public static void ReadSequence(System.ReadOnlySpan source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int contentOffset, out int contentLength, out int bytesConsumed, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; } public static void ReadSetOf(System.ReadOnlySpan source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int contentOffset, out int contentLength, out int bytesConsumed, bool skipSortOrderValidation = false, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; } public static System.DateTimeOffset ReadUtcTime(System.ReadOnlySpan source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int bytesConsumed, int twoDigitYearMax = 2049, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; } + public static bool TryDecodeLength(System.ReadOnlySpan source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int? decodedLength, out int bytesConsumed) { throw null; } public static bool TryReadBitString(System.ReadOnlySpan source, System.Span destination, System.Formats.Asn1.AsnEncodingRules ruleSet, out int unusedBitCount, out int bytesConsumed, out int bytesWritten, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; } public static bool TryReadCharacterString(System.ReadOnlySpan source, System.Span destination, System.Formats.Asn1.AsnEncodingRules ruleSet, System.Formats.Asn1.UniversalTagNumber encodingType, out int bytesConsumed, out int charsWritten, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; } public static bool TryReadCharacterStringBytes(System.ReadOnlySpan source, System.Span destination, System.Formats.Asn1.AsnEncodingRules ruleSet, System.Formats.Asn1.Asn1Tag expectedTag, out int bytesConsumed, out int bytesWritten) { throw null; } diff --git a/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnDecoder.cs b/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnDecoder.cs index 6a0bc94a94786b..28e55b114beca5 100644 --- a/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnDecoder.cs +++ b/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnDecoder.cs @@ -206,6 +206,86 @@ private static ReadOnlySpan GetPrimitiveContentSpan( return ret; } + /// + /// Decodes the data in as a length value under the specified + /// encoding rules. + /// + /// The buffer containing encoded data. + /// The encoding constraints to use when interpreting the data. + /// + /// When this method returns, the number of bytes from the beginning of + /// that contributed to the length. + /// This parameter is treated as uninitialized. + /// + /// + /// The decoded value of the length, or if the + /// encoded length represents the indefinite length. + /// + /// + /// is not a known value. + /// + /// + /// does not decode as a length under the specified encoding rules. + /// + /// + /// This method only processes the length portion of an ASN.1/BER Tag-Length-Value triplet, + /// so needs to have already sliced off the encoded tag. + /// + public static int? DecodeLength( + ReadOnlySpan source, + AsnEncodingRules ruleSet, + out int bytesConsumed) + { + CheckEncodingRules(ruleSet); + + // Use locals for the outs to hide the intermediate calculations from an out to a field. + int? ret = ReadLength(source, ruleSet, out int read); + bytesConsumed = read; + return ret; + } + + /// + /// Attempts to decode the data in as a length value under the specified + /// encoding rules. + /// + /// The buffer containing encoded data. + /// The encoding constraints to use when interpreting the data. + /// + /// When this method returns, the decoded value of the length, or if the + /// encoded length represents the indefinite length. + /// This parameter is treated as uninitialized. + /// + /// + /// When this method returns, the number of bytes from the beginning of + /// that contributed to the length. + /// This parameter is treated as uninitialized. + /// + /// + /// if the buffer represents a valid length under the specified encoding rules; + /// otherwise, + /// + /// + /// is not a known value. + /// + /// + /// This method only processes the length portion of an ASN.1/BER Tag-Length-Value triplet, + /// so needs to have already sliced off the encoded tag. + /// + public static bool TryDecodeLength( + ReadOnlySpan source, + AsnEncodingRules ruleSet, + out int? decodedLength, + out int bytesConsumed) + { + CheckEncodingRules(ruleSet); + + // Use locals for the outs to hide the intermediate calculations from an out to a field. + bool ret = TryReadLength(source, ruleSet, out int? decoded, out int read); + bytesConsumed = read; + decodedLength = decoded; + return ret; + } + private static bool TryReadLength( ReadOnlySpan source, AsnEncodingRules ruleSet, diff --git a/src/libraries/System.Formats.Asn1/tests/Reader/ReadLength.cs b/src/libraries/System.Formats.Asn1/tests/Reader/ReadLength.cs index e2238ce7c687b8..50dd7f4f95708d 100644 --- a/src/libraries/System.Formats.Asn1/tests/Reader/ReadLength.cs +++ b/src/libraries/System.Formats.Asn1/tests/Reader/ReadLength.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection; using Test.Cryptography; using Xunit; @@ -9,15 +8,46 @@ namespace System.Formats.Asn1.Tests.Reader { public sealed class ReadLength { - private delegate Asn1Tag ReadTagAndLengthDelegate( + private static Asn1Tag ReadTagAndLength( ReadOnlySpan source, AsnEncodingRules ruleSet, out int? parsedLength, - out int bytesRead); + out int bytesRead) + { + Asn1Tag tag = Asn1Tag.Decode(source, out int tagLength); + parsedLength = AsnDecoder.DecodeLength(source.Slice(tagLength), ruleSet, out int lengthLength); + bytesRead = tagLength + lengthLength; + return tag; + } + + private static bool TryReadTagAndLength( + ReadOnlySpan source, + AsnEncodingRules ruleSet, + out Asn1Tag tag, + out int? parsedLength, + out int bytesRead) + { + Asn1Tag localTag = Asn1Tag.Decode(source, out int tagLength); + + bool read = AsnDecoder.TryDecodeLength( + source.Slice(tagLength), + ruleSet, + out parsedLength, + out int lengthLength); + + if (read) + { + tag = localTag; + bytesRead = tagLength + lengthLength; + } + else + { + tag = default; + bytesRead = default; + } - private static ReadTagAndLengthDelegate ReadTagAndLength = (ReadTagAndLengthDelegate) - typeof(AsnDecoder).GetMethod("ReadTagAndLength", BindingFlags.Static | BindingFlags.NonPublic) - .CreateDelegate(typeof(ReadTagAndLengthDelegate)); + return read; + } [Theory] [InlineData(4, 0, "0400")] @@ -39,6 +69,13 @@ public static void MinimalPrimitiveLength(int tagValue, int length, string input Assert.False(tag.IsConstructed, "tag.IsConstructed"); Assert.Equal(tagValue, tag.TagValue); Assert.Equal(length, parsedLength.Value); + + Assert.True(TryReadTagAndLength(inputBytes, rules, out tag, out parsedLength, out bytesRead)); + + Assert.Equal(inputBytes.Length, bytesRead); + Assert.False(tag.IsConstructed, "tag.IsConstructed"); + Assert.Equal(tagValue, tag.TagValue); + Assert.Equal(length, parsedLength.Value); } } @@ -51,10 +88,64 @@ public static void ReadWithUnknownRuleSet(int invalidRuleSetValue) Assert.Throws( () => new AsnReader(data, (AsnEncodingRules)invalidRuleSetValue)); + + Assert.Throws( + () => ReadTagAndLength(data, (AsnEncodingRules)invalidRuleSetValue, out _, out _)); + + Assert.Throws( + () => TryReadTagAndLength(data, (AsnEncodingRules)invalidRuleSetValue, out _, out _, out _)); + } + + private static void ReadValid( + ReadOnlySpan source, + AsnEncodingRules ruleSet, + int? expectedLength, + int expectedBytesRead = -1) + { + if (expectedBytesRead < 0) + { + expectedBytesRead = source.Length; + } + + ReadTagAndLength( + source, + ruleSet, + out int? length, + out int bytesRead); + + Assert.Equal(expectedBytesRead, bytesRead); + Assert.Equal(expectedLength, length); + + bool read = TryReadTagAndLength( + source, + ruleSet, + out _, + out length, + out bytesRead); + + Assert.True(read); + Assert.Equal(expectedBytesRead, bytesRead); + Assert.Equal(expectedLength, length); + } + + private static void ReadInvalid(byte[] source, AsnEncodingRules ruleSet) + { + Assert.Throws( + () => ReadTagAndLength(source, ruleSet, out _, out _)); + + Asn1Tag tag; + int? decodedLength; + int bytesConsumed; + + Assert.False( + TryReadTagAndLength(source, ruleSet, out tag, out decodedLength, out bytesConsumed)); + + Assert.True(tag == default); + Assert.Null(decodedLength); + Assert.Equal(0, bytesConsumed); } [Theory] - [InlineData("")] [InlineData("05")] [InlineData("0481")] [InlineData("048201")] @@ -64,8 +155,9 @@ public static void ReadWithInsufficientData(string inputHex) { byte[] inputData = inputHex.HexToByteArray(); - Assert.Throws( - () => ReadTagAndLength(inputData, AsnEncodingRules.DER, out _, out _)); + ReadInvalid(inputData, AsnEncodingRules.BER); + ReadInvalid(inputData, AsnEncodingRules.CER); + ReadInvalid(inputData, AsnEncodingRules.DER); } [Theory] @@ -73,9 +165,6 @@ public static void ReadWithInsufficientData(string inputHex) [InlineData("0xFF-BER", AsnEncodingRules.BER, "04FF")] [InlineData("0xFF-CER", AsnEncodingRules.CER, "04FF")] [InlineData("0xFF-DER", AsnEncodingRules.DER, "04FF")] - [InlineData("CER definite constructed", AsnEncodingRules.CER, "30820500")] - [InlineData("BER indefinite primitive", AsnEncodingRules.BER, "0480" + "0000")] - [InlineData("CER indefinite primitive", AsnEncodingRules.CER, "0480" + "0000")] [InlineData("DER indefinite primitive", AsnEncodingRules.DER, "0480" + "0000")] [InlineData("DER non-minimal 0", AsnEncodingRules.DER, "048100")] [InlineData("DER non-minimal 7F", AsnEncodingRules.DER, "04817F")] @@ -102,10 +191,28 @@ public static void InvalidLengths( { _ = description; byte[] inputData = inputHex.HexToByteArray(); - AsnReader reader = new AsnReader(inputData, rules); - Assert.Throws( - () => ReadTagAndLength(inputData, rules, out _, out _)); + ReadInvalid(inputData, rules); + } + + [Theory] + [InlineData("CER definite constructed", AsnEncodingRules.CER, 0x0500, 4, "30820500")] + [InlineData("BER indefinite primitive", AsnEncodingRules.BER, null, 2, "0480" + "0000")] + [InlineData("CER indefinite primitive", AsnEncodingRules.CER, null, 2, "0480" + "0000")] + public static void ContextuallyInvalidLengths( + string description, + AsnEncodingRules rules, + int? expectedLength, + int expectedBytesRead, + string inputHex) + { + // These inputs will all throw from AsnDecoder.ReadTagAndLength, but require + // the tag as context. + + _ = description; + byte[] inputData = inputHex.HexToByteArray(); + + ReadValid(inputData, rules, expectedLength, expectedBytesRead); } [Theory] @@ -117,18 +224,8 @@ public static void IndefiniteLength(AsnEncodingRules ruleSet) // NULL // End-of-Contents byte[] data = { 0x30, 0x80, 0x05, 0x00, 0x00, 0x00 }; - AsnReader reader = new AsnReader(data, ruleSet); - - Asn1Tag tag = ReadTagAndLength( - data, - ruleSet, - out int? length, - out int bytesRead); - Assert.Equal(2, bytesRead); - Assert.False(length.HasValue, "length.HasValue"); - Assert.Equal((int)UniversalTagNumber.Sequence, tag.TagValue); - Assert.True(tag.IsConstructed, "tag.IsConstructed"); + ReadValid(data, ruleSet, null, 2); } [Theory] @@ -138,42 +235,25 @@ public static void IndefiniteLength(AsnEncodingRules ruleSet) public static void BerNonMinimalLength(int expectedLength, string inputHex) { byte[] inputData = inputHex.HexToByteArray(); - AsnReader reader = new AsnReader(inputData, AsnEncodingRules.BER); - Asn1Tag tag = ReadTagAndLength( - inputData, - AsnEncodingRules.BER, - out int? length, - out int bytesRead); - - Assert.Equal(inputData.Length, bytesRead); - Assert.Equal(expectedLength, length.Value); - // ReadTagAndLength doesn't move the _data span forward. - Assert.True(reader.HasData, "reader.HasData"); + ReadValid(inputData, AsnEncodingRules.BER, expectedLength); + ReadInvalid(inputData, AsnEncodingRules.CER); + ReadInvalid(inputData, AsnEncodingRules.DER); } [Theory] - [InlineData(AsnEncodingRules.BER, 4, 0, 5, "0483000000" + "0500")] - [InlineData(AsnEncodingRules.DER, 1, 1, 2, "0101" + "FF")] - [InlineData(AsnEncodingRules.CER, 0x10, null, 2, "3080" + "0500" + "0000")] + [InlineData(AsnEncodingRules.BER, 0, 5, "0483000000" + "0500")] + [InlineData(AsnEncodingRules.DER, 1, 2, "0101" + "FF")] + [InlineData(AsnEncodingRules.CER, null, 2, "3080" + "0500" + "0000")] public static void ReadWithDataRemaining( AsnEncodingRules ruleSet, - int tagValue, int? expectedLength, int expectedBytesRead, string inputHex) { byte[] inputData = inputHex.HexToByteArray(); - Asn1Tag tag = ReadTagAndLength( - inputData, - ruleSet, - out int? length, - out int bytesRead); - - Assert.Equal(expectedBytesRead, bytesRead); - Assert.Equal(tagValue, tag.TagValue); - Assert.Equal(expectedLength, length); + ReadValid(inputData, ruleSet, expectedLength, expectedBytesRead); } } }