Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AsnDecoder {Try}DecodeLength #101141

Merged
merged 3 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/libraries/System.Formats.Asn1/ref/System.Formats.Asn1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public AsnContentException(string? message, System.Exception? inner) { }
}
public static partial class AsnDecoder
{
public static int? DecodeLength(System.ReadOnlySpan<byte> source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int bytesConsumed) { throw null; }
public static byte[] ReadBitString(System.ReadOnlySpan<byte> 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<byte> 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<byte> 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; }
Expand All @@ -74,6 +75,7 @@ public static partial class AsnDecoder
public static void ReadSequence(System.ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int? decodedLength, out int bytesConsumed) { throw null; }
public static bool TryReadBitString(System.ReadOnlySpan<byte> source, System.Span<byte> 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<byte> source, System.Span<char> 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<byte> source, System.Span<byte> destination, System.Formats.Asn1.AsnEncodingRules ruleSet, System.Formats.Asn1.Asn1Tag expectedTag, out int bytesConsumed, out int bytesWritten) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,86 @@ private static ReadOnlySpan<byte> GetPrimitiveContentSpan(
return ret;
}

/// <summary>
/// Decodes the data in <paramref name="source"/> as a length value 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 number of bytes from the beginning of <paramref name="source"/>
/// that contributed to the length.
/// This parameter is treated as uninitialized.
/// </param>
/// <returns>
/// The decoded value of the length, or <see langword="null"/> if the
/// encoded length represents the indefinite length.
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="ruleSet"/> is not a known <see cref="AsnEncodingRules"/> value.
/// </exception>
/// <exception cref="AsnContentException">
/// <paramref name="source"/> does not decode as a length under the specified encoding rules.
/// </exception>
/// <remarks>
/// This method only processes the length portion of an ASN.1/BER Tag-Length-Value triplet,
/// so <paramref name="source"/> needs to have already sliced off the encoded tag.
/// </remarks>
public static int? DecodeLength(
vcsjones marked this conversation as resolved.
Show resolved Hide resolved
ReadOnlySpan<byte> 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;
}

/// <summary>
/// Attempts to decode the data in <paramref name="source"/> as a length value 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="decodedLength">
/// When this method returns, the decoded value of the length, or <see langword="null"/> if the
/// encoded length represents the indefinite length.
/// This parameter is treated as uninitialized.
/// </param>
/// <param name="bytesConsumed">
/// When this method returns, the number of bytes from the beginning of <paramref name="source"/>
/// that contributed to the length.
/// This parameter is treated as uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if the buffer represents a valid length under the specified encoding rules;
/// otherwise, <see langword="false"/>
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="ruleSet"/> is not a known <see cref="AsnEncodingRules"/> value.
/// </exception>
/// <remarks>
/// This method only processes the length portion of an ASN.1/BER Tag-Length-Value triplet,
/// so <paramref name="source"/> needs to have already sliced off the encoded tag.
/// </remarks>
public static bool TryDecodeLength(
ReadOnlySpan<byte> 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<byte> source,
AsnEncodingRules ruleSet,
Expand Down
180 changes: 130 additions & 50 deletions src/libraries/System.Formats.Asn1/tests/Reader/ReadLength.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,53 @@
// 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;

namespace System.Formats.Asn1.Tests.Reader
{
public sealed class ReadLength
{
private delegate Asn1Tag ReadTagAndLengthDelegate(
private static Asn1Tag ReadTagAndLength(
ReadOnlySpan<byte> 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<byte> 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")]
Expand All @@ -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);
}
}

Expand All @@ -51,10 +88,64 @@ public static void ReadWithUnknownRuleSet(int invalidRuleSetValue)

Assert.Throws<ArgumentOutOfRangeException>(
() => new AsnReader(data, (AsnEncodingRules)invalidRuleSetValue));

Assert.Throws<ArgumentOutOfRangeException>(
() => ReadTagAndLength(data, (AsnEncodingRules)invalidRuleSetValue, out _, out _));

Assert.Throws<ArgumentOutOfRangeException>(
() => TryReadTagAndLength(data, (AsnEncodingRules)invalidRuleSetValue, out _, out _, out _));
}

private static void ReadValid(
ReadOnlySpan<byte> 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<AsnContentException>(
() => 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")]
Expand All @@ -64,18 +155,16 @@ public static void ReadWithInsufficientData(string inputHex)
{
byte[] inputData = inputHex.HexToByteArray();

Assert.Throws<AsnContentException>(
() => ReadTagAndLength(inputData, AsnEncodingRules.DER, out _, out _));
ReadInvalid(inputData, AsnEncodingRules.BER);
ReadInvalid(inputData, AsnEncodingRules.CER);
ReadInvalid(inputData, AsnEncodingRules.DER);
}

[Theory]
[InlineData("DER indefinite constructed", AsnEncodingRules.DER, "3080" + "0500" + "0000")]
[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")]
Expand All @@ -102,10 +191,28 @@ public static void InvalidLengths(
{
_ = description;
byte[] inputData = inputHex.HexToByteArray();
AsnReader reader = new AsnReader(inputData, rules);

Assert.Throws<AsnContentException>(
() => 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]
Expand All @@ -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]
Expand All @@ -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);
}
}
}
Loading