Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,18 @@
<Compile Include="$(CommonPath)System\Obsoletions.cs" Link="Common\System\Obsoletions.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
<Compile Include="System\Formats\Asn1\AsnCharacterStringEncodings.downlevel.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<Compile Include="System\Formats\Asn1\AsnCharacterStringEncodings.net.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == '$(NetCoreAppCurrent)'">
<ProjectReference Include="$(LibrariesProjectRoot)System.Collections\src\System.Collections.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Memory\src\System.Memory.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Numerics.Vectors\src\System.Numerics.Vectors.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime\src\System.Runtime.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime.InteropServices\src\System.Runtime.InteropServices.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime.Numerics\src\System.Runtime.Numerics.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public override unsafe int GetChars(byte* bytes, int byteCount, char* chars, int
}
}

internal sealed class IA5Encoding : RestrictedAsciiStringEncoding
internal sealed class IA5Encoding : RestrictedAsciiRangeEncoding
{
// T-REC-X.680-201508 sec 41, Table 8.
// ISO International Register of Coded Character Sets to be used with Escape Sequences 001
Expand All @@ -133,7 +133,7 @@ internal IA5Encoding()
}
}

internal sealed class VisibleStringEncoding : RestrictedAsciiStringEncoding
internal sealed class VisibleStringEncoding : RestrictedAsciiRangeEncoding
{
// T-REC-X.680-201508 sec 41, Table 8.
// ISO International Register of Coded Character Sets to be used with Escape Sequences 006
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Formats.Asn1
{
internal abstract class RestrictedAsciiRangeEncoding : RestrictedAsciiStringEncoding
{
protected RestrictedAsciiRangeEncoding(byte minCharAllowed, byte maxCharAllowed)
: base(minCharAllowed, maxCharAllowed)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Numerics;
using System.Runtime.InteropServices;

namespace System.Formats.Asn1
{
internal abstract class RestrictedAsciiRangeEncoding : SpanBasedEncoding
{
private readonly byte _minCharAllowed;
private readonly byte _range;

protected RestrictedAsciiRangeEncoding(byte minCharAllowed, byte maxCharAllowed)
{
Debug.Assert(minCharAllowed <= maxCharAllowed);
Debug.Assert(maxCharAllowed <= 0x7F);

_minCharAllowed = minCharAllowed;
_range = (byte)(maxCharAllowed - minCharAllowed);
}

public override int GetMaxByteCount(int charCount)
{
return charCount;
}

public override int GetMaxCharCount(int byteCount)
{
return byteCount;
}

protected override int GetBytes(ReadOnlySpan<char> chars, Span<byte> bytes, bool write)
{
int position = 0;

if (chars.Length >= Vector<byte>.Count && Vector.IsHardwareAccelerated)
{
position = GetBytesVectorized(chars, bytes, write);
}

for (; position < chars.Length; position++)
{
char c = chars[position];

if (!IsAllowed(c))
{
EncoderFallback.CreateFallbackBuffer().Fallback(c, position);

Debug.Fail("Fallback should have thrown");
throw new InvalidOperationException();
}

if (write)
{
bytes[position] = (byte)c;
}
}

return chars.Length;
}

protected override int GetChars(ReadOnlySpan<byte> bytes, Span<char> chars, bool write)
{
int position = 0;

if (bytes.Length >= Vector<byte>.Count && Vector.IsHardwareAccelerated)
{
position = GetCharsVectorized(bytes, chars, write);
}

for (; position < bytes.Length; position++)
{
byte b = bytes[position];

if (!IsAllowed(b))
{
DecoderFallback.CreateFallbackBuffer().Fallback(
new[] { b },
position);

Debug.Fail("Fallback should have thrown");
throw new InvalidOperationException();
}

if (write)
{
chars[position] = (char)b;
}
}

return bytes.Length;
}

// The vectorization is left out of the GetChars and GetBytes directly to not regress the code size
// and register allocation for small inputs. Instead they are extracted methods.
private int GetBytesVectorized(ReadOnlySpan<char> chars, Span<byte> bytes, bool write)
{
int available = write ? Math.Min(chars.Length, bytes.Length) : chars.Length;
int vectorizedLength = available - (available % Vector<byte>.Count);
int position = 0;

Debug.Assert(Vector<byte>.Count == 2 * Vector<ushort>.Count);

// Revisit this cast when Vector<char> is supported: https://github.com/dotnet/runtime/issues/127611
ReadOnlySpan<ushort> source = MemoryMarshal.Cast<char, ushort>(chars);
Vector<ushort> minCharAllowed = new Vector<ushort>(_minCharAllowed);
Vector<ushort> range = new Vector<ushort>(_range);

for (; position < vectorizedLength; position += Vector<byte>.Count)
{
Vector<ushort> lower = new Vector<ushort>(source.Slice(position));
Vector<ushort> upper = new Vector<ushort>(source.Slice(position + Vector<ushort>.Count));

if (!IsAllowed(lower, minCharAllowed, range) || !IsAllowed(upper, minCharAllowed, range))
{
// If any element in the vector is not allowed, we break out and return the position before the
// current vector's width so that it goes down the scalar path. The scalar path will determine the
// precise location of the invalid element.
Comment thread
vcsjones marked this conversation as resolved.
break;
}

if (write)
{
Vector.Narrow(lower, upper).CopyTo(bytes.Slice(position));
}
}

return position;
}

private int GetCharsVectorized(ReadOnlySpan<byte> bytes, Span<char> chars, bool write)
{
int available = write ? Math.Min(bytes.Length, chars.Length) : bytes.Length;
int vectorizedLength = available - (available % Vector<byte>.Count);
int position = 0;

Debug.Assert(Vector<byte>.Count == 2 * Vector<ushort>.Count);

// Revisit this cast when Vector<char> is supported: https://github.com/dotnet/runtime/issues/127611
Span<ushort> destination = write ? MemoryMarshal.Cast<char, ushort>(chars) : Span<ushort>.Empty;
Vector<byte> minCharAllowed = new Vector<byte>(_minCharAllowed);
Vector<byte> range = new Vector<byte>(_range);

for (; position < vectorizedLength; position += Vector<byte>.Count)
{
Vector<byte> source = new Vector<byte>(bytes.Slice(position));

if (!IsAllowed(source, minCharAllowed, range))
{
// If any element in the vector is not allowed, we break out and return the position before the
// current vector's width so that it goes down the scalar path. The scalar path will determine the
// precise location of the invalid element.
break;
}

if (write)
{
Vector.Widen(source, out Vector<ushort> lower, out Vector<ushort> upper);
lower.CopyTo(destination.Slice(position));
upper.CopyTo(destination.Slice(position + Vector<ushort>.Count));
}
}

return position;
}

private bool IsAllowed(byte value)
{
return (byte)(value - _minCharAllowed) <= _range;
}

private bool IsAllowed(char value)
{
return (uint)(value - _minCharAllowed) <= _range;
}

private static bool IsAllowed(Vector<byte> value, Vector<byte> minCharAllowed, Vector<byte> range)
{
Vector<byte> offset = value - minCharAllowed;
return Vector.LessThanOrEqualAll(offset, range);
}

private static bool IsAllowed(Vector<ushort> value, Vector<ushort> minCharAllowed, Vector<ushort> range)
{
Vector<ushort> offset = value - minCharAllowed;
return Vector.LessThanOrEqualAll(offset, range);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// 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;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Test.Cryptography;
using Xunit;
using X509KeyUsageCSharpStyle=System.Formats.Asn1.Tests.Reader.ReadNamedBitListBase.X509KeyUsageCSharpStyle;
Expand All @@ -11,6 +15,95 @@ namespace System.Formats.Asn1.Tests.Reader
{
public static class ComprehensiveReadTests
{
public static IEnumerable<object[]> VectorBoundaryLengths
{
get
{
yield return new object[] { Vector<byte>.Count - 1 };
yield return new object[] { Vector<byte>.Count };
yield return new object[] { Vector<byte>.Count + 1 };
}
}

[Theory]
[MemberData(nameof(VectorBoundaryLengths))]
public static void ReadVisibleString_DoesNotAccessOutsideBounds(int payloadLength)
{
AssertExtensions.LessThan(payloadLength, 128);

using BoundedMemory<byte> encoded = BoundedMemory.Allocate<byte>(payloadLength + 2);
using BoundedMemory<char> destination = BoundedMemory.Allocate<char>(payloadLength);

encoded.Span[0] = (byte)UniversalTagNumber.VisibleString;
encoded.Span[1] = (byte)payloadLength;
encoded.Span.Slice(2).Fill((byte)'A');
encoded.MakeReadonly();

Assert.True(
AsnDecoder.TryReadCharacterString(
encoded.Span,
destination.Span,
AsnEncodingRules.DER,
UniversalTagNumber.VisibleString,
out int bytesConsumed,
out int charsWritten));
Assert.Equal(encoded.Length, bytesConsumed);
Assert.Equal(payloadLength, charsWritten);
AssertExtensions.FilledWith('A', destination.Span);
}

[Fact]
public static void ReadVisibleString_VectorSizedRange()
{
const int PayloadLength = 128;
byte[] encoded = new byte[PayloadLength + 3];
char[] expected = new char[PayloadLength];
encoded[0] = (byte)UniversalTagNumber.VisibleString;
encoded[1] = 0x81;
encoded[2] = PayloadLength;

for (int i = 0; i < PayloadLength; i++)
{
byte value = i % 2 == 0 ? (byte)0x20 : (byte)0x7E;
encoded[i + 3] = value;
expected[i] = (char)value;
}

Assert.Equal(
new string(expected),
AsnDecoder.ReadCharacterString(
encoded,
AsnEncodingRules.DER,
UniversalTagNumber.VisibleString,
out int bytesConsumed));
Assert.Equal(encoded.Length, bytesConsumed);
}

[Theory]
[InlineData(0x1F, 10)]
[InlineData(0x7F, 10)]
[InlineData(0x1F, 128)]
[InlineData(0x7F, 128)]
public static void ReadVisibleString_Invalid(byte invalidValue, int invalidIndex)
{
const int PayloadLength = 129;
byte[] encoded = new byte[PayloadLength + 3];
encoded[0] = (byte)UniversalTagNumber.VisibleString;
encoded[1] = 0x81;
encoded[2] = PayloadLength;
encoded.AsSpan(3).Fill((byte)'A');
encoded[invalidIndex + 3] = invalidValue;

AsnContentException exception = Assert.Throws<AsnContentException>(
() => AsnDecoder.ReadCharacterString(
encoded,
AsnEncodingRules.DER,
UniversalTagNumber.VisibleString,
out _));
DecoderFallbackException fallback = Assert.IsType<DecoderFallbackException>(exception.InnerException);
Assert.Equal(invalidIndex, fallback.Index);
}

[Fact]
public static void ReadMicrosoftComCert()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ internal abstract AsnReaderWrapper CreateWrapper(
"0000",
"Dr. & Mrs. Smith-Jones & children",
},
new object[]
{
AsnEncodingRules.BER,
"1640" + new string('4', 128),
new string('D', 64),
},
};

[Theory]
Expand Down Expand Up @@ -372,6 +378,7 @@ private void TryCopyIA5String_Throws_Helper(AsnEncodingRules ruleSet, byte[] inp
[InlineData("Bad IA5 value", AsnEncodingRules.BER, "1602E280")]
[InlineData("Bad IA5 value", AsnEncodingRules.CER, "1602E280")]
[InlineData("Bad IA5 value", AsnEncodingRules.DER, "1602E280")]
[InlineData("Bad IA5 value after vector prefix", AsnEncodingRules.BER, "1621414141414141414141414141414141414141414141414141414141414141414180")]
[InlineData("Wrong Tag", AsnEncodingRules.BER, "04024869")]
public void GetIA5String_Throws(
string description,
Expand Down Expand Up @@ -431,6 +438,7 @@ public void GetIA5String_Throws(
[InlineData("NonEmpty Null", AsnEncodingRules.CER, "3680000100")]
[InlineData("LongLength Null", AsnEncodingRules.BER, "3680008100")]
[InlineData("Bad IA5 value", AsnEncodingRules.BER, "1602E280")]
[InlineData("Bad IA5 value after vector prefix", AsnEncodingRules.BER, "1621414141414141414141414141414141414141414141414141414141414141414180")]
public void TryCopyIA5String_Throws(
string description,
AsnEncodingRules ruleSet,
Expand Down
Loading