Skip to content

Commit

Permalink
Add AsnDecoder {Try}DecodeLength
Browse files Browse the repository at this point in the history
  • Loading branch information
bartonjs authored Apr 17, 2024
1 parent 4965f21 commit 81ca1c4
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 50 deletions.
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(
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);
}
}
}

0 comments on commit 81ca1c4

Please sign in to comment.