20 Commits
1.4.1 ... 1.5.0

Author SHA1 Message Date
Matt Nadareski
ca1cb5a66e Bump version 2024-11-26 14:42:59 -05:00
Matt Nadareski
4c6ef33af2 Update OID and add tests 2024-11-26 13:16:02 -05:00
Matt Nadareski
eb519af488 Use collection expression in OID 2024-11-26 12:58:40 -05:00
Matt Nadareski
402fe65e20 Sync modified OIDIRI to regular 2024-11-26 12:57:45 -05:00
Matt Nadareski
c22f0f2c6e Update static ASN1 and add tests 2024-11-26 12:55:58 -05:00
Matt Nadareski
92d5a097a8 Write TLV tests, update code accordingly 2024-11-26 12:47:15 -05:00
Matt Nadareski
d722f1fad2 Comment out V_ASN1_UNIVERSAL 2024-11-26 11:58:20 -05:00
Matt Nadareski
a570f9a96a Modifiers should come after values 2024-11-26 11:56:41 -05:00
Matt Nadareski
d4a118ba04 Add non-formatting tests for TLV 2024-11-26 11:49:31 -05:00
Matt Nadareski
9ceea4f85a Normalize exceptions 2024-11-26 11:49:07 -05:00
Matt Nadareski
a0a7f23b6c Simplify unknown format printing 2024-11-26 11:48:14 -05:00
Matt Nadareski
bca1754d42 0 is not an unknown type 2024-11-26 11:45:48 -05:00
Matt Nadareski
a6a281deb8 Assemble length byte-wise instead of trickery 2024-11-26 11:39:57 -05:00
Matt Nadareski
37bc010762 Enable stream construction, fix safety issues 2024-11-26 10:37:26 -05:00
Matt Nadareski
0a217e5e6b Fix field access in TLV 2024-11-26 09:57:01 -05:00
Matt Nadareski
73e7b035b8 Update IO to 1.6.0 2024-11-26 09:54:31 -05:00
Matt Nadareski
7276ab2199 Update test package versions 2024-11-26 09:53:44 -05:00
Matt Nadareski
fef4d30add Add skeleton test project 2024-11-26 09:51:28 -05:00
Matt Nadareski
e632f8537b Bump version 2024-11-20 15:55:54 -05:00
Matt Nadareski
ce8c4b1831 Fix issue with 0 unused bits 2024-11-20 15:03:26 -05:00
13 changed files with 717 additions and 89 deletions

View File

@@ -0,0 +1,61 @@
using System;
using System.IO;
using Xunit;
namespace SabreTools.ASN1.Test
{
public class AbstractSyntaxNotationOneTests
{
[Fact]
public void Parse_EmptyArray_Throws()
{
byte[] data = [];
Assert.Throws<InvalidDataException>(() => AbstractSyntaxNotationOne.Parse(data, 0));
}
[Fact]
public void Parse_ValidArrayNegativeIndex_Throws()
{
byte[] data = [0x00];
Assert.Throws<IndexOutOfRangeException>(() => AbstractSyntaxNotationOne.Parse(data, -1));
}
[Fact]
public void Parse_ValidArrayOverIndex_Throws()
{
byte[] data = [0x00];
Assert.Throws<IndexOutOfRangeException>(() => AbstractSyntaxNotationOne.Parse(data, 10));
}
[Fact]
public void Parse_ValidMinimalArray()
{
byte[] data = [0x00];
var tlvs = AbstractSyntaxNotationOne.Parse(data, 0);
var tlv = Assert.Single(tlvs);
Assert.Equal(ASN1Type.V_ASN1_EOC, tlv.Type);
Assert.Equal(default, tlv.Length);
Assert.Null(tlv.Value);
}
[Fact]
public void Parse_EmptyStream_Throws()
{
Stream data = new MemoryStream([], 0, 0, false, false);
Assert.Throws<InvalidDataException>(() => AbstractSyntaxNotationOne.Parse(data));
}
[Fact]
public void Parse_ValidMinimalStream()
{
Stream data = new MemoryStream([0x00]);
var tlvs = AbstractSyntaxNotationOne.Parse(data);
var tlv = Assert.Single(tlvs);
Assert.Equal(ASN1Type.V_ASN1_EOC, tlv.Type);
Assert.Equal(default, tlv.Length);
Assert.Null(tlv.Value);
}
}
}

View File

@@ -0,0 +1,110 @@
using Xunit;
namespace SabreTools.ASN1.Test
{
// These tests are known to be incomplete due to the sheer number
// of possible OIDs that exist. The tests below are a minimal
// representation of functionality to guarantee proper behavior
// not necessarily absolute outputs
public class ObjectIdentifierTests
{
#region ASN.1
[Fact]
public void ASN1Notation_AlwaysNull()
{
ulong[]? values = null;
string? actual = ObjectIdentifier.ParseOIDToASN1Notation(values);
Assert.Null(actual);
}
#endregion
#region Dot Notation
[Fact]
public void DotNotation_NullValues_Null()
{
ulong[]? values = null;
string? actual = ObjectIdentifier.ParseOIDToDotNotation(values);
Assert.Null(actual);
}
[Fact]
public void DotNotation_EmptyValues_Null()
{
ulong[]? values = [];
string? actual = ObjectIdentifier.ParseOIDToDotNotation(values);
Assert.Null(actual);
}
[Fact]
public void DotNotation_Values_Formatted()
{
string expected = "0.1.2.3";
ulong[]? values = [0, 1, 2, 3];
string? actual = ObjectIdentifier.ParseOIDToDotNotation(values);
Assert.Equal(expected, actual);
}
#endregion
#region Modified OID-IRI
[Fact]
public void ModifiedOIDIRI_NullValues_Null()
{
ulong[]? values = null;
string? actual = ObjectIdentifier.ParseOIDToModifiedOIDIRI(values);
Assert.Null(actual);
}
[Fact]
public void ModifiedOIDIRI_EmptyValues_Null()
{
ulong[]? values = [];
string? actual = ObjectIdentifier.ParseOIDToModifiedOIDIRI(values);
Assert.Null(actual);
}
[Fact]
public void ModifiedOIDIRI_Values_Formatted()
{
string expected = "/ITU-T/[question]/2/3";
ulong[]? values = [0, 1, 2, 3];
string? actual = ObjectIdentifier.ParseOIDToModifiedOIDIRI(values);
Assert.Equal(expected, actual);
}
#endregion
#region OID-IRI
[Fact]
public void OIDIRI_NullValues_Null()
{
ulong[]? values = null;
string? actual = ObjectIdentifier.ParseOIDToOIDIRINotation(values);
Assert.Null(actual);
}
[Fact]
public void OIDIRI_EmptyValues_Null()
{
ulong[]? values = [];
string? actual = ObjectIdentifier.ParseOIDToOIDIRINotation(values);
Assert.Null(actual);
}
[Fact]
public void OIDIRI_Values_Formatted()
{
string expected = "/ITU-T/1/2/3";
ulong[]? values = [0, 1, 2, 3];
string? actual = ObjectIdentifier.ParseOIDToOIDIRINotation(values);
Assert.Equal(expected, actual);
}
#endregion
}
}

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0;net9.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<NoWarn>NU1903</NoWarn>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.ASN1\SabreTools.ASN1.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,322 @@
using System;
using System.IO;
using Xunit;
namespace SabreTools.ASN1.Test
{
public class TypeLengthValueTests
{
#region Construction
[Fact]
public void Constructor_EmptyArray_Throws()
{
int index = 0;
byte[] data = [];
Assert.Throws<InvalidDataException>(() => new TypeLengthValue(data, ref index));
}
[Fact]
public void Constructor_ValidArrayNegativeIndex_Throws()
{
int index = -1;
byte[] data = [0x00];
Assert.Throws<IndexOutOfRangeException>(() => new TypeLengthValue(data, ref index));
}
[Fact]
public void Constructor_ValidArrayOverIndex_Throws()
{
int index = 10;
byte[] data = [0x00];
Assert.Throws<IndexOutOfRangeException>(() => new TypeLengthValue(data, ref index));
}
[Fact]
public void Constructor_ValidMinimalArray()
{
int index = 0;
byte[] data = [0x00];
var tlv = new TypeLengthValue(data, ref index);
Assert.Equal(ASN1Type.V_ASN1_EOC, tlv.Type);
Assert.Equal(default, tlv.Length);
Assert.Null(tlv.Value);
}
[Fact]
public void Constructor_EmptyStream_Throws()
{
Stream data = new MemoryStream([], 0, 0, false, false);
Assert.Throws<InvalidDataException>(() => new TypeLengthValue(data));
}
[Fact]
public void Constructor_ValidMinimalStream()
{
Stream data = new MemoryStream([0x00]);
var tlv = new TypeLengthValue(data);
Assert.Equal(ASN1Type.V_ASN1_EOC, tlv.Type);
Assert.Equal(default, tlv.Length);
Assert.Null(tlv.Value);
}
[Fact]
public void Constructor_ValidBoolean()
{
Stream data = new MemoryStream([0x01, 0x01, 0x01]);
var tlv = new TypeLengthValue(data);
Assert.Equal(ASN1Type.V_ASN1_BOOLEAN, tlv.Type);
Assert.Equal(1UL, tlv.Length);
Assert.NotNull(tlv.Value);
byte[]? valueAsArray = tlv.Value as byte[];
Assert.NotNull(valueAsArray);
byte actual = Assert.Single(valueAsArray);
Assert.Equal(0x01, actual);
}
[Theory]
[InlineData(new byte[] { 0x26, 0x81, 0x03, 0x01, 0x01, 0x01 })]
[InlineData(new byte[] { 0x26, 0x82, 0x00, 0x03, 0x01, 0x01, 0x01 })]
[InlineData(new byte[] { 0x26, 0x83, 0x00, 0x00, 0x03, 0x01, 0x01, 0x01 })]
[InlineData(new byte[] { 0x26, 0x84, 0x00, 0x00, 0x00, 0x03, 0x01, 0x01, 0x01 })]
[InlineData(new byte[] { 0x26, 0x85, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x01, 0x01 })]
[InlineData(new byte[] { 0x26, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x01, 0x01 })]
[InlineData(new byte[] { 0x26, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x01, 0x01 })]
[InlineData(new byte[] { 0x26, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x01, 0x01 })]
public void Constructor_ComplexValue(byte[] arr)
{
Stream data = new MemoryStream(arr);
var tlv = new TypeLengthValue(data);
Assert.Equal(ASN1Type.V_ASN1_CONSTRUCTED | ASN1Type.V_ASN1_OBJECT, tlv.Type);
Assert.Equal(3UL, tlv.Length);
Assert.NotNull(tlv.Value);
TypeLengthValue[]? valueAsArray = tlv.Value as TypeLengthValue[];
Assert.NotNull(valueAsArray);
TypeLengthValue actual = Assert.Single(valueAsArray);
Assert.Equal(ASN1Type.V_ASN1_BOOLEAN, actual.Type);
Assert.Equal(1UL, actual.Length);
Assert.NotNull(actual.Value);
}
[Theory]
[InlineData(new byte[] { 0x26, 0x80 })]
[InlineData(new byte[] { 0x26, 0x89 })]
public void Constructor_ComplexValueInvalidLength_Throws(byte[] arr)
{
Stream data = new MemoryStream(arr);
Assert.Throws<InvalidOperationException>(() => new TypeLengthValue(data));
}
#endregion
#region Formatting
[Fact]
public void Format_EOC()
{
string expected = "Type: V_ASN1_EOC";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_EOC, 0, null);
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ZeroLength()
{
string expected = "Type: V_ASN1_NULL, Length: 0";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_NULL, 0, null);
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_InvalidConstructed()
{
string expected = "Type: V_ASN1_OBJECT, V_ASN1_CONSTRUCTED, Length: 1, Value: [INVALID DATA TYPE]";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_OBJECT | ASN1Type.V_ASN1_CONSTRUCTED, 1, (object?)false);
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidConstructed()
{
string expected = "Type: V_ASN1_OBJECT, V_ASN1_CONSTRUCTED, Length: 3, Value:\n Type: V_ASN1_BOOLEAN, Length: 1, Value: True";
var boolTlv = new TypeLengthValue(ASN1Type.V_ASN1_BOOLEAN, 1, new byte[] { 0x01 });
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_OBJECT | ASN1Type.V_ASN1_CONSTRUCTED, 3, new TypeLengthValue[] { boolTlv });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_InvalidDataType()
{
string expected = "Type: V_ASN1_OBJECT, Length: 1, Value: [INVALID DATA TYPE]";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_OBJECT, 1, (object?)false);
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_InvalidLength()
{
string expected = "Type: V_ASN1_NULL, Length: 1, Value: [NO DATA]";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_NULL, 1, Array.Empty<byte>());
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_InvalidBooleanLength()
{
string expected = "Type: V_ASN1_BOOLEAN, Length: 2 [Expected length of 1], Value: True";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_BOOLEAN, 2, new byte[] { 0x01 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_InvalidBooleanArrayLength()
{
string expected = "Type: V_ASN1_BOOLEAN, Length: 1 [Expected value length of 1], Value: True";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_BOOLEAN, 1, new byte[] { 0x01, 0x00 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidBoolean()
{
string expected = "Type: V_ASN1_BOOLEAN, Length: 1, Value: True";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_BOOLEAN, 1, new byte[] { 0x01 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidInteger()
{
string expected = "Type: V_ASN1_INTEGER, Length: 1, Value: 1";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_INTEGER, 1, new byte[] { 0x01 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidBitString_NoBits()
{
string expected = "Type: V_ASN1_BIT_STRING, Length: 1, Value with 0 unused bits";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_BIT_STRING, 1, new byte[] { 0x00 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidBitString_Bits()
{
string expected = "Type: V_ASN1_BIT_STRING, Length: 1, Value with 1 unused bits: 01";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_BIT_STRING, 1, new byte[] { 0x01, 0x01 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidOctetString()
{
string expected = "Type: V_ASN1_OCTET_STRING, Length: 1, Value: 01";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_OCTET_STRING, 1, new byte[] { 0x01 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidObject()
{
string expected = "Type: V_ASN1_OBJECT, Length: 3, Value: 0.1.2.3 (/ITU-T/1/2/3)";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_OBJECT, 3, new byte[] { 0x01, 0x02, 0x03 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidUTF8String()
{
string expected = "Type: V_ASN1_UTF8STRING, Length: 3, Value: ABC";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_UTF8STRING, 3, new byte[] { 0x41, 0x42, 0x43 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidPrintableString()
{
string expected = "Type: V_ASN1_PRINTABLESTRING, Length: 3, Value: ABC";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_PRINTABLESTRING, 3, new byte[] { 0x41, 0x42, 0x43 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidTeletexString()
{
string expected = "Type: V_ASN1_TELETEXSTRING, Length: 3, Value: ABC";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_TELETEXSTRING, 3, new byte[] { 0x41, 0x42, 0x43 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidIA5String()
{
string expected = "Type: V_ASN1_IA5STRING, Length: 3, Value: ABC";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_IA5STRING, 3, new byte[] { 0x41, 0x42, 0x43 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_InvalidUTCTime()
{
string expected = "Type: V_ASN1_UTCTIME, Length: 3, Value: ABC";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_UTCTIME, 3, new byte[] { 0x41, 0x42, 0x43 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidUTCTime()
{
string expected = "Type: V_ASN1_UTCTIME, Length: 3, Value: 1980-01-01 00:00:00";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_UTCTIME, 3, new byte[] { 0x31, 0x39, 0x38, 0x30, 0x2D, 0x30, 0x31, 0x2D, 0x30, 0x31, 0x20, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x3A, 0x30, 0x30 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidBmpString()
{
string expected = "Type: V_ASN1_BMPSTRING, Length: 6, Value: ABC";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_BMPSTRING, 6, new byte[] { 0x41, 0x00, 0x42, 0x00, 0x43, 0x00 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
[Fact]
public void Format_ValidUnformatted()
{
string expected = "Type: V_ASN1_OBJECT_DESCRIPTOR, Length: 1, Value: 01";
var tlv = new TypeLengthValue(ASN1Type.V_ASN1_OBJECT_DESCRIPTOR, 1, new byte[] { 0x01 });
string actual = tlv.Format();
Assert.Equal(expected, actual);
}
#endregion
}
}

View File

@@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.ASN1", "SabreTools.ASN1\SabreTools.ASN1.csproj", "{88EA40DD-E313-479C-94EA-0AB948DB4720}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.ASN1.Test", "SabreTools.ASN1.Test\SabreTools.ASN1.Test.csproj", "{5C740920-9505-4B58-AC17-2967559F6AE7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -18,5 +20,9 @@ Global
{88EA40DD-E313-479C-94EA-0AB948DB4720}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88EA40DD-E313-479C-94EA-0AB948DB4720}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88EA40DD-E313-479C-94EA-0AB948DB4720}.Release|Any CPU.Build.0 = Release|Any CPU
{5C740920-9505-4B58-AC17-2967559F6AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5C740920-9505-4B58-AC17-2967559F6AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C740920-9505-4B58-AC17-2967559F6AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C740920-9505-4B58-AC17-2967559F6AE7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
namespace SabreTools.ASN1
{
@@ -12,16 +14,38 @@ namespace SabreTools.ASN1
/// </summary>
/// <param name="data">Byte array representing the data</param>
/// <param name="pointer">Current pointer into the data</param>
/// <returns></returns>
public static List<TypeLengthValue> Parse(byte[] data, int pointer)
{
// If the data is invalid
if (data.Length == 0)
throw new InvalidDataException(nameof(data));
if (pointer < 0 || pointer >= data.Length)
throw new IndexOutOfRangeException(nameof(pointer));
using var stream = new MemoryStream(data);
stream.Seek(pointer, SeekOrigin.Begin);
return Parse(stream);
}
/// <summary>
/// Parse a stream into a DER-encoded ASN.1 structure
/// </summary>
/// <param name="data">Stream representing the data</param>
public static List<TypeLengthValue> Parse(Stream data)
{
// If the data is invalid
if (data.Length == 0 || !data.CanRead)
throw new InvalidDataException(nameof(data));
if (data.Position < 0 || data.Position >= data.Length)
throw new IndexOutOfRangeException(nameof(data));
// Create the output list to return
var topLevelValues = new List<TypeLengthValue>();
// Loop through the data and return all top-level values
while (pointer < data.Length)
while (data.Position < data.Length)
{
var topLevelValue = new TypeLengthValue(data, ref pointer);
var topLevelValue = new TypeLengthValue(data);
topLevelValues.Add(topLevelValue);
}

View File

@@ -8,17 +8,6 @@ namespace SabreTools.ASN1
[Flags]
public enum ASN1Type : byte
{
#region Modifiers
V_ASN1_UNIVERSAL = 0x00,
V_ASN1_PRIMITIVE_TAG = 0x1F,
V_ASN1_CONSTRUCTED = 0x20,
V_ASN1_APPLICATION = 0x40,
V_ASN1_CONTEXT_SPECIFIC = 0x80,
V_ASN1_PRIVATE = 0xC0,
#endregion
#region Types
V_ASN1_EOC = 0x00,
@@ -51,5 +40,19 @@ namespace SabreTools.ASN1
V_ASN1_BMPSTRING = 0x1E,
#endregion
#region Modifiers
// Commented out because it is the default
// and can interfere with V_ASN1_EOC
// V_ASN1_UNIVERSAL = 0x00,
V_ASN1_PRIMITIVE_TAG = 0x1F,
V_ASN1_CONSTRUCTED = 0x20,
V_ASN1_APPLICATION = 0x40,
V_ASN1_CONTEXT_SPECIFIC = 0x80,
V_ASN1_PRIVATE = 0xC0,
#endregion
}
}

View File

@@ -9,10 +9,9 @@ namespace SabreTools.ASN1
/// Parse an OID in separated-value notation into ASN.1 notation
/// </summary>
/// <param name="values">List of values to check against</param>
/// <param name="index">Current index into the list</param>
/// <returns>ASN.1 formatted string, if possible</returns>
/// <remarks>
public static string? ParseOIDToASN1Notation(ulong[]? values, ref int index)
public static string? ParseOIDToASN1Notation(ulong[]? values)
{
// TODO: Once the modified OID-IRI formatting is done, make an ASN.1 notation version
return null;

View File

@@ -84,7 +84,7 @@ namespace SabreTools.ASN1
#region Start
var oidPath = $"/{values[index]}";
var oidPath = string.Empty;
switch (values[index++])
{
case 0: goto oid_0;

View File

@@ -73,7 +73,7 @@ namespace SabreTools.ASN1
#region Start
var oidPath = $"/{values[index]}";
var oidPath = string.Empty;
switch (values[index++])
{
case 0: goto oid_0;

View File

@@ -24,7 +24,7 @@ namespace SabreTools.ASN1
int firstNode = Math.DivRem(data[0], 40, out int secondNode);
// Create a list for all nodes
List<ulong> nodes = new List<ulong> { (ulong)firstNode, (ulong)secondNode };
List<ulong> nodes = [(ulong)firstNode, (ulong)secondNode];
// All other nodes are encoded uniquely
int offset = 1;
@@ -65,7 +65,7 @@ namespace SabreTools.ASN1
nodes.Add(dotValue);
}
return nodes.ToArray();
return [.. nodes];
}
}
}

View File

@@ -6,7 +6,7 @@
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.4.1</Version>
<Version>1.5.0</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
@@ -20,6 +20,10 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="SabreTools.ASN1.Test" />
</ItemGroup>
<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup>
@@ -30,7 +34,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.IO" Version="1.5.1" />
<PackageReference Include="SabreTools.IO" Version="1.6.0" />
</ItemGroup>
</Project>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Text;
using SabreTools.IO.Extensions;
@@ -26,6 +27,16 @@ namespace SabreTools.ASN1
/// </summary>
public object? Value { get; private set; }
/// <summary>
/// Manual constructor
/// </summary>
public TypeLengthValue(ASN1Type type, ulong length, object? value)
{
Type = type;
Length = length;
Value = value;
}
/// <summary>
/// Read from the source data array at an index
/// </summary>
@@ -33,38 +44,26 @@ namespace SabreTools.ASN1
/// <param name="index">Index within the array to read at</param>
public TypeLengthValue(byte[] data, ref int index)
{
// Get the type and modifiers
Type = (ASN1Type)data[index++];
// If the data is invalid
if (data.Length == 0)
throw new InvalidDataException(nameof(data));
if (index < 0 || index >= data.Length)
throw new IndexOutOfRangeException(nameof(index));
// If we have an end indicator, we just return
if (Type == ASN1Type.V_ASN1_EOC)
return;
using var stream = new MemoryStream(data);
stream.Seek(index, SeekOrigin.Begin);
if (!Parse(stream))
throw new InvalidDataException(nameof(data));
}
// Get the length of the value
Length = ReadLength(data, ref index);
// Read the value
#if NET20 || NET35
if ((Type & ASN1Type.V_ASN1_CONSTRUCTED) != 0)
#else
if (Type.HasFlag(ASN1Type.V_ASN1_CONSTRUCTED))
#endif
{
var valueList = new List<TypeLengthValue>();
int currentIndex = index;
while (index < currentIndex + (int)Length)
{
valueList.Add(new TypeLengthValue(data, ref index));
}
Value = valueList.ToArray();
}
else
{
// TODO: Get more granular based on type
Value = data.ReadBytes(ref index, (int)Length);
}
/// <summary>
/// Read from the source data stream
/// </summary>
/// <param name="data">Stream representing data to read</param>
public TypeLengthValue(Stream data)
{
if (!Parse(data))
throw new InvalidDataException(nameof(data));
}
/// <summary>
@@ -77,10 +76,6 @@ namespace SabreTools.ASN1
// Create the left-padding string
string padding = new(' ', paddingLevel);
// If we have an invalid item
if (Type == 0)
return $"{padding}UNKNOWN TYPE";
// Create the string builder
var formatBuilder = new StringBuilder();
@@ -124,14 +119,21 @@ namespace SabreTools.ASN1
formatBuilder.Append(", Value: [INVALID DATA TYPE]");
return formatBuilder.ToString();
}
else if (valueAsByteArray.Length == 0)
{
formatBuilder.Append(", Value: [NO DATA]");
return formatBuilder.ToString();
}
// If we have a primitive type
switch (Type)
{
/// <see href="https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-boolean"/>
case ASN1Type.V_ASN1_BOOLEAN:
if (Length > 1 || valueAsByteArray.Length > 1)
if (Length > 1)
formatBuilder.Append($" [Expected length of 1]");
else if (valueAsByteArray.Length > 1)
formatBuilder.Append($" [Expected value length of 1]");
bool booleanValue = valueAsByteArray[0] != 0x00;
formatBuilder.Append($", Value: {booleanValue}");
@@ -148,7 +150,10 @@ namespace SabreTools.ASN1
case ASN1Type.V_ASN1_BIT_STRING:
// TODO: Read into a BitArray and print that out instead?
int unusedBits = valueAsByteArray[0];
formatBuilder.Append($", Value with {unusedBits} unused bits: {BitConverter.ToString(valueAsByteArray, 1).Replace('-', ' ')}");
if (unusedBits == 0)
formatBuilder.Append($", Value with {unusedBits} unused bits");
else
formatBuilder.Append($", Value with {unusedBits} unused bits: {BitConverter.ToString(valueAsByteArray, 1).Replace('-', ' ')}");
break;
/// <see href="https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-octet-string"/>
@@ -191,7 +196,7 @@ namespace SabreTools.ASN1
case ASN1Type.V_ASN1_UTCTIME:
string utctimeString = Encoding.ASCII.GetString(valueAsByteArray);
if (DateTime.TryParse(utctimeString, out DateTime utctimeDateTime))
formatBuilder.Append($", Value: {utctimeDateTime}");
formatBuilder.Append($", Value: {utctimeDateTime:yyyy-MM-dd HH:mm:ss}");
else
formatBuilder.Append($", Value: {utctimeString}");
break;
@@ -202,7 +207,7 @@ namespace SabreTools.ASN1
break;
default:
formatBuilder.Append($", Value (Unknown Format): {BitConverter.ToString(Value as byte[] ?? []).Replace('-', ' ')}");
formatBuilder.Append($", Value: {BitConverter.ToString(valueAsByteArray).Replace('-', ' ')}");
break;
}
@@ -210,20 +215,70 @@ namespace SabreTools.ASN1
return formatBuilder.ToString();
}
/// <summary>
/// Parse a stream into TLV data
/// </summary>
/// <param name="data">Stream representing data to read</param>
/// <returns>Indication if parsing was successful</returns>
private bool Parse(Stream data)
{
// If the data is invalid
if (data.Length == 0 || !data.CanRead)
return false;
if (data.Position < 0 || data.Position >= data.Length)
throw new IndexOutOfRangeException(nameof(data));
// Get the type and modifiers
Type = (ASN1Type)data.ReadByteValue();
// If we have an end indicator, we just return
if (Type == ASN1Type.V_ASN1_EOC)
return true;
// Get the length of the value
Length = ReadLength(data);
// Read the value
#if NET20 || NET35
if ((Type & ASN1Type.V_ASN1_CONSTRUCTED) != 0)
#else
if (Type.HasFlag(ASN1Type.V_ASN1_CONSTRUCTED))
#endif
{
var valueList = new List<TypeLengthValue>();
long currentIndex = data.Position;
while (data.Position < currentIndex + (long)Length)
{
valueList.Add(new TypeLengthValue(data));
}
Value = valueList.ToArray();
}
else
{
// TODO: Get more granular based on type
Value = data.ReadBytes((int)Length);
}
return true;
}
/// <summary>
/// Reads the length field for a type
/// </summary>
/// <param name="data">Byte array representing data to read</param>
/// <param name="index">Index within the array to read at</param>
/// <param name="data">Stream representing data to read</param>
/// <returns>The length value read from the array</returns>
private static ulong ReadLength(byte[] data, ref int index)
private static ulong ReadLength(Stream data)
{
// If we have invalid data, throw an exception
if (data == null || index < 0 && index >= data.Length)
throw new ArgumentException();
// If the data is invalid
if (data.Length == 0 || !data.CanRead)
throw new InvalidDataException(nameof(data));
if (data.Position < 0 || data.Position >= data.Length)
throw new IndexOutOfRangeException(nameof(data));
// Read the first byte, assuming it's the length
byte length = data[index++];
byte length = data.ReadByteValue();
// If the bit 7 is not set, then use the value as it is
if ((length & 0x80) == 0)
@@ -231,34 +286,48 @@ namespace SabreTools.ASN1
// Otherwise, use the value as the number of remaining bytes to read
int bytesToRead = length & ~0x80;
byte[]? bytesRead = data.ReadBytes(ref index, bytesToRead) ?? throw new InvalidOperationException();
// TODO: Write extensions to read big-endian
// Reverse the bytes to be in big-endian order
Array.Reverse(bytesRead);
switch (bytesRead.Length)
// Assemble the length based on byte count
ulong fullLength = 0;
switch (bytesToRead)
{
case 1:
return bytesRead[0];
case 2:
return BitConverter.ToUInt16(bytesRead, 0);
case 3:
Array.Resize(ref bytesRead, 4);
case 8:
fullLength |= data.ReadByteValue();
fullLength <<= 8;
goto case 7;
case 7:
fullLength |= data.ReadByteValue();
fullLength <<= 8;
goto case 6;
case 6:
fullLength |= data.ReadByteValue();
fullLength <<= 8;
goto case 5;
case 5:
fullLength |= data.ReadByteValue();
fullLength <<= 8;
goto case 4;
case 4:
return BitConverter.ToUInt32(bytesRead, 0);
case 5:
case 6:
case 7:
Array.Resize(ref bytesRead, 8);
goto case 8;
case 8:
return BitConverter.ToUInt64(bytesRead, 0);
fullLength |= data.ReadByteValue();
fullLength <<= 8;
goto case 3;
case 3:
fullLength |= data.ReadByteValue();
fullLength <<= 8;
goto case 2;
case 2:
fullLength |= data.ReadByteValue();
fullLength <<= 8;
goto case 1;
case 1:
fullLength |= data.ReadByteValue();
break;
default:
throw new InvalidOperationException();
}
return fullLength;
}
}
}