diff --git a/src/libraries/System.Formats.Asn1/src/System.Formats.Asn1.csproj b/src/libraries/System.Formats.Asn1/src/System.Formats.Asn1.csproj index f7a85ae076f779..ae8c2927edeaed 100644 --- a/src/libraries/System.Formats.Asn1/src/System.Formats.Asn1.csproj +++ b/src/libraries/System.Formats.Asn1/src/System.Formats.Asn1.csproj @@ -54,9 +54,18 @@ + + + + + + + + + diff --git a/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnCharacterStringEncodings.cs b/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnCharacterStringEncodings.cs index 5bbc3fef701b81..86cdd4376915fe 100644 --- a/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnCharacterStringEncodings.cs +++ b/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnCharacterStringEncodings.cs @@ -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 @@ -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 diff --git a/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnCharacterStringEncodings.downlevel.cs b/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnCharacterStringEncodings.downlevel.cs new file mode 100644 index 00000000000000..4c674f4e7f5f74 --- /dev/null +++ b/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnCharacterStringEncodings.downlevel.cs @@ -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) + { + } + } +} diff --git a/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnCharacterStringEncodings.net.cs b/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnCharacterStringEncodings.net.cs new file mode 100644 index 00000000000000..033c72633c7b38 --- /dev/null +++ b/src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnCharacterStringEncodings.net.cs @@ -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 chars, Span bytes, bool write) + { + int position = 0; + + if (chars.Length >= Vector.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 bytes, Span chars, bool write) + { + int position = 0; + + if (bytes.Length >= Vector.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 chars, Span bytes, bool write) + { + int available = write ? Math.Min(chars.Length, bytes.Length) : chars.Length; + int vectorizedLength = available - (available % Vector.Count); + int position = 0; + + Debug.Assert(Vector.Count == 2 * Vector.Count); + + // Revisit this cast when Vector is supported: https://github.com/dotnet/runtime/issues/127611 + ReadOnlySpan source = MemoryMarshal.Cast(chars); + Vector minCharAllowed = new Vector(_minCharAllowed); + Vector range = new Vector(_range); + + for (; position < vectorizedLength; position += Vector.Count) + { + Vector lower = new Vector(source.Slice(position)); + Vector upper = new Vector(source.Slice(position + Vector.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. + break; + } + + if (write) + { + Vector.Narrow(lower, upper).CopyTo(bytes.Slice(position)); + } + } + + return position; + } + + private int GetCharsVectorized(ReadOnlySpan bytes, Span chars, bool write) + { + int available = write ? Math.Min(bytes.Length, chars.Length) : bytes.Length; + int vectorizedLength = available - (available % Vector.Count); + int position = 0; + + Debug.Assert(Vector.Count == 2 * Vector.Count); + + // Revisit this cast when Vector is supported: https://github.com/dotnet/runtime/issues/127611 + Span destination = write ? MemoryMarshal.Cast(chars) : Span.Empty; + Vector minCharAllowed = new Vector(_minCharAllowed); + Vector range = new Vector(_range); + + for (; position < vectorizedLength; position += Vector.Count) + { + Vector source = new Vector(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 lower, out Vector upper); + lower.CopyTo(destination.Slice(position)); + upper.CopyTo(destination.Slice(position + Vector.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 value, Vector minCharAllowed, Vector range) + { + Vector offset = value - minCharAllowed; + return Vector.LessThanOrEqualAll(offset, range); + } + + private static bool IsAllowed(Vector value, Vector minCharAllowed, Vector range) + { + Vector offset = value - minCharAllowed; + return Vector.LessThanOrEqualAll(offset, range); + } + } +} diff --git a/src/libraries/System.Formats.Asn1/tests/Reader/ComprehensiveReadTests.cs b/src/libraries/System.Formats.Asn1/tests/Reader/ComprehensiveReadTests.cs index ca01bed1b7157e..f2234318c3f856 100644 --- a/src/libraries/System.Formats.Asn1/tests/Reader/ComprehensiveReadTests.cs +++ b/src/libraries/System.Formats.Asn1/tests/Reader/ComprehensiveReadTests.cs @@ -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; @@ -11,6 +15,95 @@ namespace System.Formats.Asn1.Tests.Reader { public static class ComprehensiveReadTests { + public static IEnumerable VectorBoundaryLengths + { + get + { + yield return new object[] { Vector.Count - 1 }; + yield return new object[] { Vector.Count }; + yield return new object[] { Vector.Count + 1 }; + } + } + + [Theory] + [MemberData(nameof(VectorBoundaryLengths))] + public static void ReadVisibleString_DoesNotAccessOutsideBounds(int payloadLength) + { + AssertExtensions.LessThan(payloadLength, 128); + + using BoundedMemory encoded = BoundedMemory.Allocate(payloadLength + 2); + using BoundedMemory destination = BoundedMemory.Allocate(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( + () => AsnDecoder.ReadCharacterString( + encoded, + AsnEncodingRules.DER, + UniversalTagNumber.VisibleString, + out _)); + DecoderFallbackException fallback = Assert.IsType(exception.InnerException); + Assert.Equal(invalidIndex, fallback.Index); + } + [Fact] public static void ReadMicrosoftComCert() { diff --git a/src/libraries/System.Formats.Asn1/tests/Reader/ReadIA5String.cs b/src/libraries/System.Formats.Asn1/tests/Reader/ReadIA5String.cs index 8de6fc768991a0..c1e2c612f87afb 100644 --- a/src/libraries/System.Formats.Asn1/tests/Reader/ReadIA5String.cs +++ b/src/libraries/System.Formats.Asn1/tests/Reader/ReadIA5String.cs @@ -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] @@ -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, @@ -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, diff --git a/src/libraries/System.Formats.Asn1/tests/Writer/SimpleWriterTests.cs b/src/libraries/System.Formats.Asn1/tests/Writer/SimpleWriterTests.cs index f6e86a74d940dd..8e2d4a3eabfbc8 100644 --- a/src/libraries/System.Formats.Asn1/tests/Writer/SimpleWriterTests.cs +++ b/src/libraries/System.Formats.Asn1/tests/Writer/SimpleWriterTests.cs @@ -1,13 +1,87 @@ // 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.Reflection; +using System.Text; using Xunit; namespace System.Formats.Asn1.Tests.Writer { public static class SimpleWriterTests { + public static IEnumerable VectorBoundaryLengths + { + get + { + yield return new object[] { Vector.Count - 1 }; + yield return new object[] { Vector.Count }; + yield return new object[] { Vector.Count + 1 }; + } + } + + [Theory] + [MemberData(nameof(VectorBoundaryLengths))] + public static void WriteVisibleString_DoesNotAccessOutsideBounds(int payloadLength) + { + using BoundedMemory value = BoundedMemory.Allocate(payloadLength); + value.Span.Fill('A'); + value.MakeReadonly(); + + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + writer.WriteCharacterString(UniversalTagNumber.VisibleString, value.Span); + byte[] encoded = writer.Encode(); + + string decoded = AsnDecoder.ReadCharacterString( + encoded, + AsnEncodingRules.DER, + UniversalTagNumber.VisibleString, + out int bytesConsumed); + Assert.Equal(encoded.Length, bytesConsumed); + Assert.Equal(payloadLength, decoded.Length); + AssertExtensions.FilledWith('A', decoded); + } + + [Fact] + public static void WriteVisibleString_VectorSizedRange() + { + const int PayloadLength = 128; + char[] value = new char[PayloadLength]; + + for (int i = 0; i < PayloadLength; i++) + { + value[i] = i % 2 == 0 ? (char)0x20 : (char)0x7E; + } + + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + writer.WriteCharacterString(UniversalTagNumber.VisibleString, value); + byte[] encoded = writer.Encode(); + + Assert.Equal((byte)UniversalTagNumber.VisibleString, encoded[0]); + Assert.Equal(0x81, encoded[1]); + Assert.Equal(PayloadLength, encoded[2]); + Assert.Equal(new string(value), Encoding.ASCII.GetString(encoded, 3, PayloadLength)); + } + + [Theory] + [InlineData('\u001F', 10)] + [InlineData('\u007F', 10)] + [InlineData('\u001F', 128)] + [InlineData('\u007F', 128)] + public static void WriteVisibleString_Invalid(char invalidValue, int invalidIndex) + { + char[] value = new string('A', 129).ToCharArray(); + value[invalidIndex] = invalidValue; + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + EncoderFallbackException exception = Assert.Throws( + () => writer.WriteCharacterString(UniversalTagNumber.VisibleString, value)); + Assert.Equal(invalidIndex, exception.Index); + Assert.Equal(0, writer.GetEncodedLength()); + } + [Theory] [InlineData(-1)] [InlineData(3)] diff --git a/src/libraries/System.Formats.Asn1/tests/Writer/WriteIA5String.cs b/src/libraries/System.Formats.Asn1/tests/Writer/WriteIA5String.cs index ce75813ccf7965..72622f7495f645 100644 --- a/src/libraries/System.Formats.Asn1/tests/Writer/WriteIA5String.cs +++ b/src/libraries/System.Formats.Asn1/tests/Writer/WriteIA5String.cs @@ -53,6 +53,7 @@ public class WriteIA5String : WriteCharacterString public static IEnumerable InvalidInputs { get; } = new object[][] { new object[] { "Dr. & Mrs. Smith\u2010Jones \uFE60 children", }, + new object[] { new string('A', 64) + "\u0080", }, }; internal override void WriteString(AsnWriter writer, string s) =>