diff options
45 files changed, 18045 insertions, 2 deletions
diff --git a/src/Common/src/System/Security/Cryptography/Asn1V2.cs b/src/Common/src/System/Security/Cryptography/Asn1V2.cs new file mode 100644 index 0000000000..f897ef11ad --- /dev/null +++ b/src/Common/src/System/Security/Cryptography/Asn1V2.cs @@ -0,0 +1,738 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace System.Security.Cryptography.Asn1 +{ + // ITU-T-REC.X.680-201508 sec 4. + internal enum AsnEncodingRules + { + BER, + CER, + DER, + } + + // Uses a masked overlay of the tag class encoding. + // T-REC-X.690-201508 sec 8.1.2.2 + internal enum TagClass : byte + { + Universal = 0, + Application = 0b0100_0000, + ContextSpecific = 0b1000_0000, + Private = 0b1100_0000, + } + + // ITU-T-REC.X.680-201508 sec 8.6 + internal enum UniversalTagNumber + { + EndOfContents = 0, + Boolean = 1, + Integer = 2, + BitString = 3, + OctetString = 4, + Null = 5, + ObjectIdentifier = 6, + ObjectDescriptor = 7, + External = 8, + InstanceOf = External, + Real = 9, + Enumerated = 10, + Embedded = 11, + UTF8String = 12, + RelativeObjectIdentifier = 13, + Time = 14, + // 15 is reserved + Sequence = 16, + SequenceOf = Sequence, + Set = 17, + SetOf = Set, + NumericString = 18, + PrintableString = 19, + TeletexString = 20, + T61String = TeletexString, + VideotexString = 21, + IA5String = 22, + UtcTime = 23, + GeneralizedTime = 24, + GraphicString = 25, + VisibleString = 26, + ISO646String = VisibleString, + GeneralString = 27, + UniversalString = 28, + UnrestrictedCharacterString = 29, + BMPString = 30, + Date = 31, + TimeOfDay = 32, + DateTime = 33, + Duration = 34, + ObjectIdentifierIRI = 35, + RelativeObjectIdentifierIRI = 36, + } + + // Represents a BER-family encoded tag. + // T-REC-X.690-201508 sec 8.1.2 + internal struct Asn1Tag : IEquatable<Asn1Tag> + { + private const byte ClassMask = 0b1100_0000; + private const byte ConstructedMask = 0b0010_0000; + private const byte ControlMask = ClassMask | ConstructedMask; + private const byte TagNumberMask = 0b0001_1111; + + internal static readonly Asn1Tag EndOfContents = new Asn1Tag(0, (int)UniversalTagNumber.EndOfContents); + internal static readonly Asn1Tag Boolean = new Asn1Tag(0, (int)UniversalTagNumber.Boolean); + internal static readonly Asn1Tag Integer = new Asn1Tag(0, (int)UniversalTagNumber.Integer); + internal static readonly Asn1Tag PrimitiveBitString = new Asn1Tag(0, (int)UniversalTagNumber.BitString); + internal static readonly Asn1Tag ConstructedBitString = new Asn1Tag(ConstructedMask, (int)UniversalTagNumber.BitString); + internal static readonly Asn1Tag PrimitiveOctetString = new Asn1Tag(0, (int)UniversalTagNumber.OctetString); + internal static readonly Asn1Tag ConstructedOctetString = new Asn1Tag(ConstructedMask, (int)UniversalTagNumber.OctetString); + internal static readonly Asn1Tag Null = new Asn1Tag(0, (int)UniversalTagNumber.Null); + internal static readonly Asn1Tag ObjectIdentifier = new Asn1Tag(0, (int)UniversalTagNumber.ObjectIdentifier); + internal static readonly Asn1Tag Enumerated = new Asn1Tag(0, (int)UniversalTagNumber.Enumerated); + internal static readonly Asn1Tag Sequence = new Asn1Tag(ConstructedMask, (int)UniversalTagNumber.Sequence); + internal static readonly Asn1Tag SetOf = new Asn1Tag(ConstructedMask, (int)UniversalTagNumber.SetOf); + internal static readonly Asn1Tag UtcTime = new Asn1Tag(0, (int)UniversalTagNumber.UtcTime); + internal static readonly Asn1Tag GeneralizedTime = new Asn1Tag(0, (int)UniversalTagNumber.GeneralizedTime); + + private readonly byte _controlFlags; + private readonly int _tagValue; + + public TagClass TagClass => (TagClass)(_controlFlags & ClassMask); + public bool IsConstructed => (_controlFlags & ConstructedMask) != 0; + public int TagValue => _tagValue; + + private Asn1Tag(byte controlFlags, int tagValue) + { + _controlFlags = (byte)(controlFlags & ControlMask); + _tagValue = tagValue; + } + + public Asn1Tag(UniversalTagNumber universalTagNumber, bool isConstructed = false) + : this(isConstructed ? ConstructedMask : (byte)0, (int)universalTagNumber) + { + // T-REC-X.680-201508 sec 8.6 (Table 1) + const UniversalTagNumber ReservedIndex = (UniversalTagNumber)15; + + if (universalTagNumber < UniversalTagNumber.EndOfContents || + universalTagNumber > UniversalTagNumber.RelativeObjectIdentifierIRI || + universalTagNumber == ReservedIndex) + { + throw new ArgumentOutOfRangeException(nameof(universalTagNumber), universalTagNumber, null); + } + } + + public Asn1Tag(TagClass tagClass, int tagValue, bool isConstructed = false) + : this((byte)((byte)tagClass | (isConstructed ? ConstructedMask : 0)), tagValue) + { + if (tagClass < TagClass.Universal || tagClass > TagClass.Private) + { + throw new ArgumentOutOfRangeException(nameof(tagClass), tagClass, null); + } + } + + public Asn1Tag AsConstructed() + { + return new Asn1Tag((byte)(_controlFlags | ConstructedMask), _tagValue); + } + + public Asn1Tag AsPrimitive() + { + return new Asn1Tag((byte)(_controlFlags & ~ConstructedMask), _tagValue); + } + + public static bool TryParse(ReadOnlySpan<byte> source, out Asn1Tag tag, out int bytesRead) + { + tag = default(Asn1Tag); + bytesRead = 0; + + if (source.IsEmpty) + { + return false; + } + + byte first = source[bytesRead]; + bytesRead++; + uint tagValue = (uint)(first & TagNumberMask); + + if (tagValue == TagNumberMask) + { + // Multi-byte encoding + // T-REC-X.690-201508 sec 8.1.2.4 + const byte ContinuationFlag = 0x80; + const byte ValueMask = ContinuationFlag - 1; + + tagValue = 0; + byte current; + + do + { + if (source.Length <= bytesRead) + { + bytesRead = 0; + return false; + } + + current = source[bytesRead]; + byte currentValue = (byte)(current & ValueMask); + bytesRead++; + + // If TooBigToShift is shifted left 7, the content bit shifts out. + // So any value greater than or equal to this cannot be shifted without loss. + const int TooBigToShift = 0b00000010_00000000_00000000_00000000; + + if (tagValue >= TooBigToShift) + { + bytesRead = 0; + return false; + } + + tagValue <<= 7; + tagValue |= currentValue; + + // The first byte cannot have the value 0 (T-REC-X.690-201508 sec 8.1.2.4.2.c) + if (tagValue == 0) + { + bytesRead = 0; + return false; + } + } + while ((current & ContinuationFlag) == ContinuationFlag); + + // This encoding is only valid for tag values greater than 30. + // (T-REC-X.690-201508 sec 8.1.2.3, 8.1.2.4) + if (tagValue <= 30) + { + bytesRead = 0; + return false; + } + + // There's not really any ambiguity, but prevent negative numbers from showing up. + if (tagValue > int.MaxValue) + { + bytesRead = 0; + return false; + } + } + + Debug.Assert(bytesRead > 0); + tag = new Asn1Tag(first, (int)tagValue); + return true; + } + + public int CalculateEncodedSize() + { + const int SevenBits = 0b0111_1111; + const int FourteenBits = 0b0011_1111_1111_1111; + const int TwentyOneBits = 0b0001_1111_1111_1111_1111_1111; + const int TwentyEightBits = 0b0000_1111_1111_1111_1111_1111_1111_1111; + + if (TagValue < TagNumberMask) + return 1; + if (TagValue <= SevenBits) + return 2; + if (TagValue <= FourteenBits) + return 3; + if (TagValue <= TwentyOneBits) + return 4; + if (TagValue <= TwentyEightBits) + return 5; + + return 6; + } + + public bool TryWrite(Span<byte> destination, out int bytesWritten) + { + int spaceRequired = CalculateEncodedSize(); + + if (destination.Length < spaceRequired) + { + bytesWritten = 0; + return false; + } + + if (spaceRequired == 1) + { + byte value = (byte)(_controlFlags | TagValue); + destination[0] = value; + bytesWritten = 1; + return true; + } + + byte firstByte = (byte)(_controlFlags | TagNumberMask); + destination[0] = firstByte; + + int remaining = TagValue; + int idx = spaceRequired - 1; + + while (remaining > 0) + { + int segment = remaining & 0x7F; + + // The last byte doesn't get the marker, which we write first. + if (remaining != TagValue) + { + segment |= 0x80; + } + + Debug.Assert(segment <= byte.MaxValue); + destination[idx] = (byte)segment; + remaining >>= 7; + idx--; + } + + Debug.Assert(idx == 0); + bytesWritten = spaceRequired; + return true; + } + + public bool Equals(Asn1Tag other) + { + return _controlFlags == other._controlFlags && _tagValue == other._tagValue; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is Asn1Tag && Equals((Asn1Tag)obj); + } + + public override int GetHashCode() + { + // Most TagValue values will be in the 0-30 range, + // the GetHashCode value only has collisions when TagValue is + // between 2^29 and uint.MaxValue + return (_controlFlags << 24) ^ _tagValue; + } + + public static bool operator ==(Asn1Tag left, Asn1Tag right) + { + return left.Equals(right); + } + + public static bool operator !=(Asn1Tag left, Asn1Tag right) + { + return !left.Equals(right); + } + + public override string ToString() + { + const string ConstructedPrefix = "Constructed "; + string classAndValue; + + if (TagClass == TagClass.Universal) + { + classAndValue = ((UniversalTagNumber)TagValue).ToString(); + } + else + { + classAndValue = TagClass + "-" + TagValue; + } + + if (IsConstructed) + { + return ConstructedPrefix + classAndValue; + } + + return classAndValue; + } + } + + internal static class AsnCharacterStringEncodings + { + private static readonly Text.Encoding s_utf8Encoding = new UTF8Encoding(false, throwOnInvalidBytes: true); + private static readonly Text.Encoding s_bmpEncoding = new BMPEncoding(); + private static readonly Text.Encoding s_ia5Encoding = new IA5Encoding(); + private static readonly Text.Encoding s_visibleStringEncoding = new VisibleStringEncoding(); + private static readonly Text.Encoding s_printableStringEncoding = new PrintableStringEncoding(); + + internal static Text.Encoding GetEncoding(UniversalTagNumber encodingType) + { + switch (encodingType) + { + case UniversalTagNumber.UTF8String: + return s_utf8Encoding; + case UniversalTagNumber.PrintableString: + return s_printableStringEncoding; + case UniversalTagNumber.IA5String: + return s_ia5Encoding; + case UniversalTagNumber.VisibleString: + return s_visibleStringEncoding; + case UniversalTagNumber.BMPString: + return s_bmpEncoding; + default: + throw new ArgumentOutOfRangeException(nameof(encodingType), encodingType, null); + } + } + } + + internal abstract class SpanBasedEncoding : Text.Encoding + { + protected SpanBasedEncoding() + : base(0, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback) + { + } + + protected abstract int GetBytes(ReadOnlySpan<char> chars, Span<byte> bytes, bool write); + protected abstract int GetChars(ReadOnlySpan<byte> bytes, Span<char> chars, bool write); + + public override int GetByteCount(char[] chars, int index, int count) + { + return GetByteCount(new ReadOnlySpan<char>(chars, index, count)); + } + + public override unsafe int GetByteCount(char* chars, int count) + { + return GetByteCount(new ReadOnlySpan<char>(chars, count)); + } + + public override int GetByteCount(string s) + { + return GetByteCount(s.AsReadOnlySpan()); + } + + private int GetByteCount(ReadOnlySpan<char> chars) + { + return GetBytes(chars, Span<byte>.Empty, write: false); + } + + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) + { + return GetBytes( + new ReadOnlySpan<char>(chars, charIndex, charCount), + new Span<byte>(bytes, byteIndex, bytes.Length - byteIndex), + write: true); + } + + public override unsafe int GetBytes(char* chars, int charCount, byte* bytes, int byteCount) + { + return GetBytes( + new ReadOnlySpan<char>(chars, charCount), + new Span<byte>(bytes, byteCount), + write: true); + } + + public override int GetCharCount(byte[] bytes, int index, int count) + { + return GetCharCount(new ReadOnlySpan<byte>(bytes, index, count)); + } + + public override unsafe int GetCharCount(byte* bytes, int count) + { + return GetCharCount(new ReadOnlySpan<byte>(bytes, count)); + } + + private int GetCharCount(ReadOnlySpan<byte> bytes) + { + return GetChars(bytes, Span<char>.Empty, write: false); + } + + public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) + { + return GetChars( + new ReadOnlySpan<byte>(bytes, byteIndex, byteCount), + new Span<char>(chars, charIndex, chars.Length - charIndex), + write: true); + } + + public override unsafe int GetChars(byte* bytes, int byteCount, char* chars, int charCount) + { + return GetChars( + new ReadOnlySpan<byte>(bytes, byteCount), + new Span<char>(chars, charCount), + write: true); + } + } + + internal class IA5Encoding : RestrictedAsciiStringEncoding + { + // T-REC-X.680-201508 sec 41, Table 8. + // ISO International Register of Coded Character Sets to be used with Escape Sequences 001 + // is ASCII 0x00 - 0x1F + // ISO International Register of Coded Character Sets to be used with Escape Sequences 006 + // is ASCII 0x21 - 0x7E + // Space is ASCII 0x20, delete is ASCII 0x7F. + // + // The net result is all of 7-bit ASCII + internal IA5Encoding() + : base(0x00, 0x7F) + { + } + } + + internal class VisibleStringEncoding : RestrictedAsciiStringEncoding + { + // T-REC-X.680-201508 sec 41, Table 8. + // ISO International Register of Coded Character Sets to be used with Escape Sequences 006 + // is ASCII 0x21 - 0x7E + // Space is ASCII 0x20. + internal VisibleStringEncoding() + : base(0x20, 0x7E) + { + } + } + + internal class PrintableStringEncoding : RestrictedAsciiStringEncoding + { + // T-REC-X.680-201508 sec 41.4 + internal PrintableStringEncoding() + : base("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 '()+,-./:=?") + { + } + } + + internal abstract class RestrictedAsciiStringEncoding : SpanBasedEncoding + { + private readonly bool[] _isAllowed; + + protected RestrictedAsciiStringEncoding(byte minCharAllowed, byte maxCharAllowed) + { + Debug.Assert(minCharAllowed <= maxCharAllowed); + Debug.Assert(maxCharAllowed <= 0x7F); + + bool[] isAllowed = new bool[0x80]; + + for (byte charCode = minCharAllowed; charCode <= maxCharAllowed; charCode++) + { + isAllowed[charCode] = true; + } + + _isAllowed = isAllowed; + } + + protected RestrictedAsciiStringEncoding(IEnumerable<char> allowedChars) + { + bool[] isAllowed = new bool[0x7F]; + + foreach (char c in allowedChars) + { + if (c >= isAllowed.Length) + { + throw new ArgumentOutOfRangeException(nameof(allowedChars)); + } + + Debug.Assert(isAllowed[c] == false); + isAllowed[c] = true; + } + + _isAllowed = isAllowed; + } + + 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) + { + if (chars.IsEmpty) + { + return 0; + } + + for (int i = 0; i < chars.Length; i++) + { + char c = chars[i]; + + if ((uint)c >= (uint)_isAllowed.Length || !_isAllowed[c]) + { + EncoderFallback.CreateFallbackBuffer().Fallback(c, i); + + Debug.Fail("Fallback should have thrown"); + throw new CryptographicException(); + } + + if (write) + { + bytes[i] = (byte)c; + } + } + + return chars.Length; + } + + protected override int GetChars(ReadOnlySpan<byte> bytes, Span<char> chars, bool write) + { + if (bytes.IsEmpty) + { + return 0; + } + + for (int i = 0; i < bytes.Length; i++) + { + byte b = bytes[i]; + + if ((uint)b >= (uint)_isAllowed.Length || !_isAllowed[b]) + { + DecoderFallback.CreateFallbackBuffer().Fallback( + new[] { b }, + i); + + Debug.Fail("Fallback should have thrown"); + throw new CryptographicException(); + } + + if (write) + { + chars[i] = (char)b; + } + } + + return bytes.Length; + } + } + + /// <summary> + /// Big-Endian UCS-2 encoding (the same as UTF-16BE, but disallowing surrogate pairs to leave plane 0) + /// </summary> + // T-REC-X.690-201508 sec 8.23.8 says to see ISO/IEC 10646:2003 section 13.1. + // ISO/IEC 10646:2003 sec 13.1 says each character is represented by "two octets". + // ISO/IEC 10646:2003 sec 6.3 says that when serialized as octets to use big endian. + internal class BMPEncoding : SpanBasedEncoding + { + protected override int GetBytes(ReadOnlySpan<char> chars, Span<byte> bytes, bool write) + { + if (chars.IsEmpty) + { + return 0; + } + + int writeIdx = 0; + + for (int i = 0; i < chars.Length; i++) + { + char c = chars[i]; + + if (char.IsSurrogate(c)) + { + EncoderFallback.CreateFallbackBuffer().Fallback(c, i); + + Debug.Fail("Fallback should have thrown"); + throw new CryptographicException(); + } + + ushort val16 = c; + + if (write) + { + bytes[writeIdx + 1] = (byte)val16; + bytes[writeIdx] = (byte)(val16 >> 8); + } + + writeIdx += 2; + } + + return writeIdx; + } + + protected override int GetChars(ReadOnlySpan<byte> bytes, Span<char> chars, bool write) + { + if (bytes.IsEmpty) + { + return 0; + } + + if (bytes.Length % 2 != 0) + { + DecoderFallback.CreateFallbackBuffer().Fallback( + bytes.Slice(bytes.Length - 1).ToArray(), + bytes.Length - 1); + + Debug.Fail("Fallback should have thrown"); + throw new CryptographicException(); + } + + int writeIdx = 0; + + for (int i = 0; i < bytes.Length; i += 2) + { + int val = bytes[i] << 8 | bytes[i + 1]; + char c = (char)val; + + if (char.IsSurrogate(c)) + { + DecoderFallback.CreateFallbackBuffer().Fallback( + bytes.Slice(i, 2).ToArray(), + i); + + Debug.Fail("Fallback should have thrown"); + throw new CryptographicException(); + } + + if (write) + { + chars[writeIdx] = c; + } + + writeIdx++; + } + + return writeIdx; + } + + public override int GetMaxByteCount(int charCount) + { + checked + { + return charCount * 2; + } + } + + public override int GetMaxCharCount(int byteCount) + { + return byteCount / 2; + } + } + + internal class SetOfValueComparer : IComparer<ReadOnlyMemory<byte>> + { + internal static SetOfValueComparer Instance { get; } = new SetOfValueComparer(); + + public int Compare(ReadOnlyMemory<byte> x, ReadOnlyMemory<byte> y) + { + ReadOnlySpan<byte> xSpan = x.Span; + ReadOnlySpan<byte> ySpan = y.Span; + + int min = Math.Min(x.Length, y.Length); + int diff; + + for (int i = 0; i < min; i++) + { + int xVal = xSpan[i]; + byte yVal = ySpan[i]; + diff = xVal - yVal; + + if (diff != 0) + { + return diff; + } + } + + // The sorting rules (T-REC-X.690-201508 sec 11.6) say that the shorter one + // counts as if it are padded with as many 0x00s on the right as required for + // comparison. + // + // But, since a shorter definite value will have already had the length bytes + // compared, it was already different. And a shorter indefinite value will + // have hit end-of-contents, making it already different. + // + // This is here because the spec says it should be, but no values are known + // which will make diff != 0. + diff = x.Length - y.Length; + + if (diff != 0) + { + return diff; + } + + return 0; + } + } +} diff --git a/src/Common/src/System/Security/Cryptography/AsnReader.cs b/src/Common/src/System/Security/Cryptography/AsnReader.cs new file mode 100644 index 0000000000..50d4a9f94f --- /dev/null +++ b/src/Common/src/System/Security/Cryptography/AsnReader.cs @@ -0,0 +1,2910 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.Buffers.Binary; +using System.Buffers.Text; +using System.Collections.Generic; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; + +namespace System.Security.Cryptography.Asn1 +{ + internal class AsnReader + { + // T-REC-X.690-201508 sec 9.2 + internal const int MaxCERSegmentSize = 1000; + + // T-REC-X.690-201508 sec 8.1.5 says only 0000 is legal. + private const int EndOfContentsEncodedLength = 2; + + private ReadOnlyMemory<byte> _data; + private readonly AsnEncodingRules _ruleSet; + + public bool HasData => !_data.IsEmpty; + + public AsnReader(ReadOnlyMemory<byte> data, AsnEncodingRules ruleSet) + { + CheckEncodingRules(ruleSet); + + _data = data; + _ruleSet = ruleSet; + } + + public void ThrowIfNotEmpty() + { + if (HasData) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + public static bool TryPeekTag(ReadOnlySpan<byte> source, out Asn1Tag tag, out int bytesRead) + { + return Asn1Tag.TryParse(source, out tag, out bytesRead); + } + + public Asn1Tag PeekTag() + { + if (TryPeekTag(_data.Span, out Asn1Tag tag, out int bytesRead)) + { + return tag; + } + + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + private static bool TryReadLength( + ReadOnlySpan<byte> source, + AsnEncodingRules ruleSet, + out int? length, + out int bytesRead) + { + length = null; + bytesRead = 0; + + CheckEncodingRules(ruleSet); + + if (source.IsEmpty) + { + return false; + } + + // T-REC-X.690-201508 sec 8.1.3 + + byte lengthOrLengthLength = source[bytesRead]; + bytesRead++; + const byte MultiByteMarker = 0x80; + + // 0x00-0x7F are direct length values. + // 0x80 is BER/CER indefinite length. + // 0x81-0xFE says that the length takes the next 1-126 bytes. + // 0xFF is forbidden. + if (lengthOrLengthLength == MultiByteMarker) + { + // T-REC-X.690-201508 sec 10.1 (DER: Length forms) + if (ruleSet == AsnEncodingRules.DER) + { + bytesRead = 0; + return false; + } + + // Null length == indefinite. + return true; + } + + if (lengthOrLengthLength < MultiByteMarker) + { + length = lengthOrLengthLength; + return true; + } + + if (lengthOrLengthLength == 0xFF) + { + bytesRead = 0; + return false; + } + + byte lengthLength = (byte)(lengthOrLengthLength & ~MultiByteMarker); + + // +1 for lengthOrLengthLength + if (lengthLength + 1 > source.Length) + { + bytesRead = 0; + return false; + } + + // T-REC-X.690-201508 sec 9.1 (CER: Length forms) + // T-REC-X.690-201508 sec 10.1 (DER: Length forms) + bool minimalRepresentation = + ruleSet == AsnEncodingRules.DER || ruleSet == AsnEncodingRules.CER; + + // The ITU-T specifications tecnically allow lengths up to ((2^128) - 1), but + // since Span's length is a signed Int32 we're limited to identifying memory + // that is within ((2^31) - 1) bytes of the tag start. + if (minimalRepresentation && lengthLength > sizeof(int)) + { + bytesRead = 0; + return false; + } + + uint parsedLength = 0; + + for (int i = 0; i < lengthLength; i++) + { + byte current = source[bytesRead]; + bytesRead++; + + if (parsedLength == 0) + { + if (minimalRepresentation && current == 0) + { + bytesRead = 0; + return false; + } + + if (!minimalRepresentation && current != 0) + { + // Under BER rules we could have had padding zeros, so + // once the first data bits come in check that we fit within + // sizeof(int) due to Span bounds. + + if (lengthLength - i > sizeof(int)) + { + bytesRead = 0; + return false; + } + } + } + + parsedLength <<= 8; + parsedLength |= current; + } + + // This value cannot be represented as a Span length. + if (parsedLength > int.MaxValue) + { + bytesRead = 0; + return false; + } + + if (minimalRepresentation && parsedLength < MultiByteMarker) + { + bytesRead = 0; + return false; + } + + Debug.Assert(bytesRead > 0); + length = (int)parsedLength; + return true; + } + + internal Asn1Tag ReadTagAndLength(out int? contentsLength, out int bytesRead) + { + if (TryPeekTag(_data.Span, out Asn1Tag tag, out int tagBytesRead) && + TryReadLength(_data.Slice(tagBytesRead).Span, _ruleSet, out int? length, out int lengthBytesRead)) + { + int allBytesRead = tagBytesRead + lengthBytesRead; + + if (tag.IsConstructed) + { + // T-REC-X.690-201508 sec 9.1 (CER: Length forms) says constructed is always indefinite. + if (_ruleSet == AsnEncodingRules.CER && length != null) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + else if (length == null) + { + // T-REC-X.690-201508 sec 8.1.3.2 says primitive encodings must use a definite form. + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + bytesRead = allBytesRead; + contentsLength = length; + return tag; + } + + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + private static void ValidateEndOfContents(Asn1Tag tag, int? length, int headerLength) + { + // T-REC-X.690-201508 sec 8.1.5 excludes the BER 8100 length form for 0. + if (tag.IsConstructed || length != 0 || headerLength != EndOfContentsEncodedLength) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + /// <summary> + /// Get the number of bytes between the start of <paramref name="source" /> and + /// the End-of-Contents marker + /// </summary> + private int SeekEndOfContents(ReadOnlyMemory<byte> source) + { + ReadOnlyMemory<byte> cur = source; + int totalLen = 0; + + AsnReader tmpReader = new AsnReader(cur, _ruleSet); + // Our reader is bounded by int.MaxValue. + // The most aggressive data input would be a one-byte tag followed by + // indefinite length "ad infinitum", which would be half the input. + // So the depth marker can never overflow the signed integer space. + int depth = 1; + + while (tmpReader.HasData) + { + Asn1Tag tag = tmpReader.ReadTagAndLength(out int? length, out int bytesRead); + + if (tag == Asn1Tag.EndOfContents) + { + ValidateEndOfContents(tag, length, bytesRead); + + depth--; + + if (depth == 0) + { + // T-REC-X.690-201508 sec 8.1.1.1 / 8.1.1.3 indicate that the + // End-of-Contents octets are "after" the contents octets, not + // "at the end" of them, so we don't include these bytes in the + // accumulator. + return totalLen; + } + } + + // We found another indefinite length, that means we need to find another + // EndOfContents marker to balance it out. + if (length == null) + { + depth++; + tmpReader._data = tmpReader._data.Slice(bytesRead); + totalLen += bytesRead; + } + else + { + // This will throw a CryptographicException if the length exceeds our bounds. + ReadOnlyMemory<byte> tlv = Slice(tmpReader._data, 0, bytesRead + length.Value); + + // No exception? Then slice the data and continue. + tmpReader._data = tmpReader._data.Slice(tlv.Length); + totalLen += tlv.Length; + } + } + + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + /// <summary> + /// Get a ReadOnlyMemory view of the next encoded value without consuming it. + /// For indefinite length encodings this includes the End of Contents marker. + /// </summary> + /// <returns>A ReadOnlyMemory view of the next encoded value.</returns> + /// <exception cref="CryptographicException"> + /// The reader is positioned at a point where the tag or length is invalid + /// under the current encoding rules. + /// </exception> + /// <seealso cref="PeekContentBytes"/> + /// <seealso cref="GetEncodedValue"/> + public ReadOnlyMemory<byte> PeekEncodedValue() + { + Asn1Tag tag = ReadTagAndLength(out int? length, out int bytesRead); + + if (length == null) + { + int contentsLength = SeekEndOfContents(_data.Slice(bytesRead)); + return Slice(_data, 0, bytesRead + contentsLength + EndOfContentsEncodedLength); + } + + return Slice(_data, 0, bytesRead + length.Value); + } + + /// <summary> + /// Get a ReadOnlyMemory view of the content octets (bytes) of the next encoded + /// value without consuming it. + /// </summary> + /// <returns>A ReadOnlyMemory view of the contents octets of the next encoded value.</returns> + /// <exception cref="CryptographicException"> + /// The reader is positioned at a point where the tag or length is invalid + /// under the current encoding rules. + /// </exception> + /// <seealso cref="PeekEncodedValue"/> + public ReadOnlyMemory<byte> PeekContentBytes() + { + Asn1Tag tag = ReadTagAndLength(out int? length, out int bytesRead); + + if (length == null) + { + return Slice(_data, bytesRead, SeekEndOfContents(_data.Slice(bytesRead))); + } + + return Slice(_data, bytesRead, length.Value); + } + + /// <summary> + /// Get a ReadOnlyMemory view of the next encoded value, and move the reader past it. + /// For an indefinite length encoding this includes the End of Contents marker. + /// </summary> + /// <returns>A ReadOnlyMemory view of the next encoded value.</returns> + /// <seealso cref="PeekEncodedValue"/> + public ReadOnlyMemory<byte> GetEncodedValue() + { + ReadOnlyMemory<byte> encodedValue = PeekEncodedValue(); + _data = _data.Slice(encodedValue.Length); + return encodedValue; + } + + private static bool ReadBooleanValue( + ReadOnlySpan<byte> source, + AsnEncodingRules ruleSet) + { + // T-REC-X.690-201508 sec 8.2.1 + if (source.Length != 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + byte val = source[0]; + + // T-REC-X.690-201508 sec 8.2.2 + if (val == 0) + { + return false; + } + + // T-REC-X.690-201508 sec 11.1 + if (val != 0xFF && (ruleSet == AsnEncodingRules.DER || ruleSet == AsnEncodingRules.CER)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return true; + } + + public bool ReadBoolean() => ReadBoolean(Asn1Tag.Boolean); + + public bool ReadBoolean(Asn1Tag expectedTag) + { + Asn1Tag tag = ReadTagAndLength(out int? length, out int headerLength); + CheckExpectedTag(tag, expectedTag, UniversalTagNumber.Boolean); + + // T-REC-X.690-201508 sec 8.2.1 + if (tag.IsConstructed) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + bool value = ReadBooleanValue( + Slice(_data, headerLength, length.Value).Span, + _ruleSet); + + _data = _data.Slice(headerLength + length.Value); + return value; + } + + private ReadOnlyMemory<byte> GetIntegerContents( + Asn1Tag expectedTag, + UniversalTagNumber tagNumber, + out int headerLength) + { + Asn1Tag tag = ReadTagAndLength(out int? length, out headerLength); + CheckExpectedTag(tag, expectedTag, tagNumber); + + // T-REC-X.690-201508 sec 8.3.1 + if (tag.IsConstructed || length < 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // Slice first so that an out of bounds value triggers a CryptographicException. + ReadOnlyMemory<byte> contents = Slice(_data, headerLength, length.Value); + ReadOnlySpan<byte> contentSpan = contents.Span; + + // T-REC-X.690-201508 sec 8.3.2 + if (contents.Length > 1) + { + ushort bigEndianValue = (ushort)(contentSpan[0] << 8 | contentSpan[1]); + const ushort RedundancyMask = 0b1111_1111_1000_0000; + ushort masked = (ushort)(bigEndianValue & RedundancyMask); + + // If the first 9 bits are all 0 or are all 1, the value is invalid. + if (masked == 0 || masked == RedundancyMask) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + return contents; + } + + public ReadOnlyMemory<byte> GetIntegerBytes() => + GetIntegerBytes(Asn1Tag.Integer); + + public ReadOnlyMemory<byte> GetIntegerBytes(Asn1Tag expectedTag) + { + ReadOnlyMemory<byte> contents = + GetIntegerContents(expectedTag, UniversalTagNumber.Integer, out int headerLength); + + _data = _data.Slice(headerLength + contents.Length); + return contents; + } + + private bool TryReadSignedInteger( + int sizeLimit, + Asn1Tag expectedTag, + UniversalTagNumber tagNumber, + out long value) + { + Debug.Assert(sizeLimit <= sizeof(long)); + + ReadOnlyMemory<byte> contents = GetIntegerContents(expectedTag, tagNumber, out int headerLength); + + if (contents.Length > sizeLimit) + { + value = 0; + return false; + } + + ReadOnlySpan<byte> contentSpan = contents.Span; + + bool isNegative = (contentSpan[0] & 0x80) != 0; + long accum = isNegative ? -1 : 0; + + for (int i = 0; i < contents.Length; i++) + { + accum <<= 8; + accum |= contentSpan[i]; + } + + _data = _data.Slice(headerLength + contents.Length); + value = accum; + return true; + } + + private bool TryReadUnsignedInteger( + int sizeLimit, + Asn1Tag expectedTag, + UniversalTagNumber tagNumber, + out ulong value) + { + Debug.Assert(sizeLimit <= sizeof(ulong)); + + ReadOnlyMemory<byte> contents = GetIntegerContents(expectedTag, tagNumber, out int headerLength); + ReadOnlySpan<byte> contentSpan = contents.Span; + int contentLength = contents.Length; + + bool isNegative = (contentSpan[0] & 0x80) != 0; + + if (isNegative) + { + value = 0; + return false; + } + + // Ignore any padding zeros. + if (contentSpan.Length > 1 && contentSpan[0] == 0) + { + contentSpan = contentSpan.Slice(1); + } + + if (contentSpan.Length > sizeLimit) + { + value = 0; + return false; + } + + ulong accum = 0; + + for (int i = 0; i < contentSpan.Length; i++) + { + accum <<= 8; + accum |= contentSpan[i]; + } + + _data = _data.Slice(headerLength + contentLength); + value = accum; + return true; + } + + public bool TryReadInt32(out int value) => + TryReadInt32(Asn1Tag.Integer, out value); + + public bool TryReadInt32(Asn1Tag expectedTag, out int value) + { + if (TryReadSignedInteger(sizeof(int), expectedTag, UniversalTagNumber.Integer, out long longValue)) + { + value = (int)longValue; + return true; + } + + value = 0; + return false; + } + + public bool TryReadUInt32(out uint value) => + TryReadUInt32(Asn1Tag.Integer, out value); + + public bool TryReadUInt32(Asn1Tag expectedTag, out uint value) + { + if (TryReadUnsignedInteger(sizeof(uint), expectedTag, UniversalTagNumber.Integer, out ulong ulongValue)) + { + value = (uint)ulongValue; + return true; + } + + value = 0; + return false; + } + + public bool TryReadInt64(out long value) => + TryReadInt64(Asn1Tag.Integer, out value); + + public bool TryReadInt64(Asn1Tag expectedTag, out long value) + { + return TryReadSignedInteger(sizeof(long), expectedTag, UniversalTagNumber.Integer, out value); + } + + public bool TryReadUInt64(out ulong value) => + TryReadUInt64(Asn1Tag.Integer, out value); + + public bool TryReadUInt64(Asn1Tag expectedTag, out ulong value) + { + return TryReadUnsignedInteger(sizeof(ulong), expectedTag, UniversalTagNumber.Integer, out value); + } + + public bool TryReadInt16(out short value) => + TryReadInt16(Asn1Tag.Integer, out value); + + public bool TryReadInt16(Asn1Tag expectedTag, out short value) + { + if (TryReadSignedInteger(sizeof(short), expectedTag, UniversalTagNumber.Integer, out long longValue)) + { + value = (short)longValue; + return true; + } + + value = 0; + return false; + } + + public bool TryReadUInt16(out ushort value) => + TryReadUInt16(Asn1Tag.Integer, out value); + + public bool TryReadUInt16(Asn1Tag expectedTag, out ushort value) + { + if (TryReadUnsignedInteger(sizeof(ushort), expectedTag, UniversalTagNumber.Integer, out ulong ulongValue)) + { + value = (ushort)ulongValue; + return true; + } + + value = 0; + return false; + } + + public bool TryReadInt8(out sbyte value) => + TryReadInt8(Asn1Tag.Integer, out value); + + public bool TryReadInt8(Asn1Tag expectedTag, out sbyte value) + { + if (TryReadSignedInteger(sizeof(sbyte), expectedTag, UniversalTagNumber.Integer, out long longValue)) + { + value = (sbyte)longValue; + return true; + } + + value = 0; + return false; + } + + public bool TryReadUInt8(out byte value) => + TryReadUInt8(Asn1Tag.Integer, out value); + + public bool TryReadUInt8(Asn1Tag expectedTag, out byte value) + { + if (TryReadUnsignedInteger(sizeof(byte), expectedTag, UniversalTagNumber.Integer, out ulong ulongValue)) + { + value = (byte)ulongValue; + return true; + } + + value = 0; + return false; + } + + private void ParsePrimitiveBitStringContents( + ReadOnlyMemory<byte> source, + out int unusedBitCount, + out ReadOnlyMemory<byte> value, + out byte normalizedLastByte) + { + // T-REC-X.690-201508 sec 9.2 + if (_ruleSet == AsnEncodingRules.CER && source.Length > MaxCERSegmentSize) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // T-REC-X.690-201508 sec 8.6.2.3 + if (source.Length == 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ReadOnlySpan<byte> sourceSpan = source.Span; + unusedBitCount = sourceSpan[0]; + + // T-REC-X.690-201508 sec 8.6.2.2 + if (unusedBitCount > 7) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (source.Length == 1) + { + // T-REC-X.690-201508 sec 8.6.2.4 + if (unusedBitCount > 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + Debug.Assert(unusedBitCount == 0); + value = ReadOnlyMemory<byte>.Empty; + normalizedLastByte = 0; + return; + } + + // Build a mask for the bits that are used so the normalized value can be computed + // + // If 3 bits are "unused" then build a mask for them to check for 0. + // -1 << 3 => 0b1111_1111 << 3 => 0b1111_1000 + int mask = -1 << unusedBitCount; + byte lastByte = sourceSpan[sourceSpan.Length - 1]; + byte maskedByte = (byte)(lastByte & mask); + + if (maskedByte != lastByte) + { + // T-REC-X.690-201508 sec 11.2.1 + if (_ruleSet == AsnEncodingRules.DER || _ruleSet == AsnEncodingRules.CER) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + normalizedLastByte = maskedByte; + value = source.Slice(1); + } + + private delegate void BitStringCopyAction( + ReadOnlyMemory<byte> value, + byte normalizedLastByte, + Span<byte> destination); + + private static void CopyBitStringValue( + ReadOnlyMemory<byte> value, + byte normalizedLastByte, + Span<byte> destination) + { + if (value.Length == 0) + { + return; + } + + value.Span.CopyTo(destination); + // Replace the last byte with the normalized answer. + destination[value.Length - 1] = normalizedLastByte; + } + + private int CountConstructedBitString(ReadOnlyMemory<byte> source, bool isIndefinite) + { + Span<byte> destination = Span<byte>.Empty; + + return ProcessConstructedBitString( + source, + destination, + null, + isIndefinite, + out _, + out _); + } + + private void CopyConstructedBitString( + ReadOnlyMemory<byte> source, + Span<byte> destination, + bool isIndefinite, + out int unusedBitCount, + out int bytesRead, + out int bytesWritten) + { + Span<byte> tmpDest = destination; + + bytesWritten = ProcessConstructedBitString( + source, + tmpDest, + (value, lastByte, dest) => CopyBitStringValue(value, lastByte, dest), + isIndefinite, + out unusedBitCount, + out bytesRead); + } + + private int ProcessConstructedBitString( + ReadOnlyMemory<byte> source, + Span<byte> destination, + BitStringCopyAction copyAction, + bool isIndefinite, + out int lastUnusedBitCount, + out int bytesRead) + { + lastUnusedBitCount = 0; + bytesRead = 0; + int lastSegmentLength = MaxCERSegmentSize; + + AsnReader tmpReader = new AsnReader(source, _ruleSet); + Stack<(AsnReader, bool, int)> readerStack = null; + int totalLength = 0; + Asn1Tag tag = Asn1Tag.ConstructedBitString; + Span<byte> curDest = destination; + + do + { + while (tmpReader.HasData) + { + tag = tmpReader.ReadTagAndLength(out int? length, out int headerLength); + + if (tag == Asn1Tag.PrimitiveBitString) + { + if (lastUnusedBitCount != 0) + { + // T-REC-X.690-201508 sec 8.6.4, only the last segment may have + // a number of bits not a multiple of 8. + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (_ruleSet == AsnEncodingRules.CER && lastSegmentLength != MaxCERSegmentSize) + { + // T-REC-X.690-201508 sec 9.2 + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + Debug.Assert(length != null); + ReadOnlyMemory<byte> encodedValue = Slice(tmpReader._data, headerLength, length.Value); + + ParsePrimitiveBitStringContents( + encodedValue, + out lastUnusedBitCount, + out ReadOnlyMemory<byte> contents, + out byte normalizedLastByte); + + int localLen = headerLength + encodedValue.Length; + tmpReader._data = tmpReader._data.Slice(localLen); + + bytesRead += localLen; + totalLength += contents.Length; + lastSegmentLength = encodedValue.Length; + + if (copyAction != null) + { + copyAction(contents, normalizedLastByte, curDest); + curDest = curDest.Slice(contents.Length); + } + } + else if (tag == Asn1Tag.EndOfContents && isIndefinite) + { + ValidateEndOfContents(tag, length, headerLength); + + bytesRead += headerLength; + + if (readerStack?.Count > 0) + { + (AsnReader topReader, bool wasIndefinite, int pushedBytesRead) = readerStack.Pop(); + topReader._data = topReader._data.Slice(bytesRead); + + bytesRead += pushedBytesRead; + isIndefinite = wasIndefinite; + tmpReader = topReader; + } + else + { + // We have matched the EndOfContents that brought us here. + break; + } + } + else if (tag == Asn1Tag.ConstructedBitString) + { + if (_ruleSet == AsnEncodingRules.CER) + { + // T-REC-X.690-201508 sec 9.2 + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (readerStack == null) + { + readerStack = new Stack<(AsnReader, bool, int)>(); + } + + readerStack.Push((tmpReader, isIndefinite, bytesRead)); + + tmpReader = new AsnReader( + Slice(tmpReader._data, headerLength, length), + _ruleSet); + + bytesRead = headerLength; + isIndefinite = (length == null); + } + else + { + // T-REC-X.690-201508 sec 8.6.4.1 (in particular, Note 2) + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + if (isIndefinite && tag != Asn1Tag.EndOfContents) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (readerStack?.Count > 0) + { + (AsnReader topReader, bool wasIndefinite, int pushedBytesRead) = readerStack.Pop(); + + tmpReader = topReader; + tmpReader._data = tmpReader._data.Slice(bytesRead); + + isIndefinite = wasIndefinite; + bytesRead += pushedBytesRead; + } + else + { + tmpReader = null; + } + } while (tmpReader != null); + + return totalLength; + } + + private bool TryCopyConstructedBitStringValue( + ReadOnlyMemory<byte> source, + Span<byte> dest, + bool isIndefinite, + out int unusedBitCount, + out int bytesRead, + out int bytesWritten) + { + // Call CountConstructedBitString to get the required byte and to verify that the + // data is well-formed before copying into dest. + int contentLength = CountConstructedBitString(source, isIndefinite); + + // Since the unused bits byte from the segments don't count, only one segment + // returns 999 (or less), the second segment bumps the count to 1000, and is legal. + // + // T-REC-X.690-201508 sec 9.2 + if (_ruleSet == AsnEncodingRules.CER && contentLength < MaxCERSegmentSize) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (dest.Length < contentLength) + { + unusedBitCount = 0; + bytesRead = 0; + bytesWritten = 0; + return false; + } + + CopyConstructedBitString( + source, + dest, + isIndefinite, + out unusedBitCount, + out bytesRead, + out bytesWritten); + + Debug.Assert(bytesWritten == contentLength); + return true; + } + + private bool TryGetPrimitiveBitStringValue( + Asn1Tag expectedTag, + out Asn1Tag actualTag, + out int? contentsLength, + out int headerLength, + out int unusedBitCount, + out ReadOnlyMemory<byte> value, + out byte normalizedLastByte) + { + actualTag = ReadTagAndLength(out contentsLength, out headerLength); + CheckExpectedTag(actualTag, expectedTag, UniversalTagNumber.BitString); + + if (actualTag.IsConstructed) + { + if (_ruleSet == AsnEncodingRules.DER) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + unusedBitCount = 0; + value = default(ReadOnlyMemory<byte>); + normalizedLastByte = 0; + return false; + } + + Debug.Assert(contentsLength.HasValue); + ReadOnlyMemory<byte> encodedValue = Slice(_data, headerLength, contentsLength.Value); + + ParsePrimitiveBitStringContents( + encodedValue, + out unusedBitCount, + out value, + out normalizedLastByte); + + return true; + } + + public bool TryGetPrimitiveBitStringValue(out int unusedBitCount, out ReadOnlyMemory<byte> contents) + => TryGetPrimitiveBitStringValue(Asn1Tag.PrimitiveBitString, out unusedBitCount, out contents); + + /// <summary> + /// Gets a ReadOnlyMemory view over the data value portion of the contents of a bit string. + /// </summary> + /// <param name="expectedTag">The expected tag to read</param> + /// <param name="unusedBitCount">The encoded value for the number of unused bits.</param> + /// <param name="value">The data value portion of the bit string contents.</param> + /// <returns> + /// <c>true</c> if the bit string uses a primitive encoding and the "unused" bits have value 0, + /// <c>false</c> otherwise. + /// </returns> + /// <exception cref="CryptographicException"> + /// <ul> + /// <li>No data remains</li> + /// <li>The tag read does not match the expected tag</li> + /// <li>The length is invalid under the chosen encoding rules</li> + /// <li>The unusedBitCount value is out of bounds</li> + /// <li>A CER or DER encoding was chosen and an "unused" bit was set to 1</li> + /// <li>A CER encoding was chosen and the primitive content length exceeds the maximum allowed</li> + /// </ul> + /// </exception> + public bool TryGetPrimitiveBitStringValue( + Asn1Tag expectedTag, + out int unusedBitCount, + out ReadOnlyMemory<byte> value) + { + bool isPrimitive = TryGetPrimitiveBitStringValue( + expectedTag, + out Asn1Tag actualTag, + out int? contentsLength, + out int headerLength, + out unusedBitCount, + out value, + out byte normalizedLastByte); + + if (isPrimitive) + { + // A BER reader which encountered a situation where an "unused" bit was not + // set to 0. + if (value.Length != 0 && normalizedLastByte != value.Span[value.Length - 1]) + { + unusedBitCount = 0; + value = default(ReadOnlyMemory<byte>); + return false; + } + + // Skip the tag+length (header) and the unused bit count byte (1) and the contents. + _data = _data.Slice(headerLength + value.Length + 1); + } + + return isPrimitive; + } + + public bool TryCopyBitStringBytes( + Span<byte> destination, + out int unusedBitCount, + out int bytesWritten) + { + return TryCopyBitStringBytes( + Asn1Tag.PrimitiveBitString, + destination, + out unusedBitCount, + out bytesWritten); + } + + public bool TryCopyBitStringBytes( + Asn1Tag expectedTag, + Span<byte> destination, + out int unusedBitCount, + out int bytesWritten) + { + if (TryGetPrimitiveBitStringValue( + expectedTag, + out Asn1Tag actualTag, + out int? contentsLength, + out int headerLength, + out unusedBitCount, + out ReadOnlyMemory<byte> value, + out byte normalizedLastByte)) + { + if (value.Length > destination.Length) + { + bytesWritten = 0; + unusedBitCount = 0; + return false; + } + + CopyBitStringValue(value, normalizedLastByte, destination); + + bytesWritten = value.Length; + // contents doesn't include the unusedBitCount value, so add one byte for that. + _data = _data.Slice(headerLength + value.Length + 1); + return true; + } + + Debug.Assert(actualTag.IsConstructed); + + bool read = TryCopyConstructedBitStringValue( + Slice(_data, headerLength, contentsLength), + destination, + contentsLength == null, + out unusedBitCount, + out int bytesRead, + out bytesWritten); + + if (read) + { + _data = _data.Slice(headerLength + bytesRead); + } + + return read; + } + + public TFlagsEnum GetNamedBitListValue<TFlagsEnum>() where TFlagsEnum : struct => + GetNamedBitListValue<TFlagsEnum>(Asn1Tag.PrimitiveBitString); + + public TFlagsEnum GetNamedBitListValue<TFlagsEnum>(Asn1Tag expectedTag) where TFlagsEnum : struct + { + Type tFlagsEnum = typeof(TFlagsEnum); + + return (TFlagsEnum)Enum.ToObject(tFlagsEnum, GetNamedBitListValue(expectedTag, tFlagsEnum)); + } + + public Enum GetNamedBitListValue(Type tFlagsEnum) => + GetNamedBitListValue(Asn1Tag.PrimitiveBitString, tFlagsEnum); + + public Enum GetNamedBitListValue(Asn1Tag expectedTag, Type tFlagsEnum) + { + // This will throw an ArgumentException if TEnum isn't an enum type, + // so we don't need to validate it. + Type backingType = tFlagsEnum.GetEnumUnderlyingType(); + + if (!tFlagsEnum.IsDefined(typeof(FlagsAttribute), false)) + { + throw new ArgumentException( + SR.Cryptography_Asn_NamedBitListRequiresFlagsEnum, + nameof(tFlagsEnum)); + } + + int sizeLimit = Marshal.SizeOf(backingType); + Span<byte> stackSpan = stackalloc byte[sizeLimit]; + ReadOnlyMemory<byte> saveData = _data; + + // If TryCopyBitStringBytes succeeds but anything else fails _data will have moved, + // so if anything throws here just move _data back to what it was. + try + { + if (!TryCopyBitStringBytes(expectedTag, stackSpan, out int unusedBitCount, out int bytesWritten)) + { + throw new CryptographicException( + SR.Format(SR.Cryptography_Asn_NamedBitListValueTooBig, tFlagsEnum.Name)); + } + + if (bytesWritten == 0) + { + // The mode isn't relevant, zero is always zero. + return (Enum)Enum.ToObject(tFlagsEnum, 0); + } + + ReadOnlySpan<byte> valueSpan = stackSpan.Slice(0, bytesWritten); + + // Now that the 0-bounds check is out of the way: + // + // T-REC-X.690-201508 sec 11.2.2 + if (_ruleSet == AsnEncodingRules.DER || + _ruleSet == AsnEncodingRules.CER) + { + byte lastByte = valueSpan[bytesWritten - 1]; + + // No unused bits tests 0x01, 1 is 0x02, 2 is 0x04, etc. + // We already know that TryCopyBitStringBytes checked that the + // declared unused bits were 0, this checks that the last "used" bit + // isn't also zero. + byte testBit = (byte)(1 << unusedBitCount); + + if ((lastByte & testBit) == 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + // Consider a NamedBitList defined as + // + // SomeList ::= BIT STRING { + // a(0), b(1), c(2), d(3), e(4), f(5), g(6), h(7), i(8), j(9), k(10) + // } + // + // The BIT STRING encoding of (a | j) is + // unusedBitCount = 6, + // contents: 0x80 0x40 (0b10000000_01000000) + // + // A the C# exposure of this structure we adhere to is + // + // [Flags] + // enum SomeList + // { + // A = 1, + // B = 1 << 1, + // C = 1 << 2, + // ... + // } + // + // Which happens to be exactly backwards from how the bits are encoded, but the complexity + // only needs to live here. + return (Enum)Enum.ToObject(tFlagsEnum, InterpretNamedBitListReversed(valueSpan)); + } + catch + { + _data = saveData; + throw; + } + } + + private static long InterpretNamedBitListReversed(ReadOnlySpan<byte> valueSpan) + { + Debug.Assert(valueSpan.Length <= sizeof(long)); + + long accum = 0; + long currentBitValue = 1; + + for (int byteIdx = 0; byteIdx < valueSpan.Length; byteIdx++) + { + byte byteVal = valueSpan[byteIdx]; + + for (int bitIndex = 7; bitIndex >= 0; bitIndex--) + { + int test = 1 << bitIndex; + + if ((byteVal & test) != 0) + { + accum |= currentBitValue; + } + + currentBitValue <<= 1; + } + } + + return accum; + } + + public ReadOnlyMemory<byte> GetEnumeratedBytes() => + GetEnumeratedBytes(Asn1Tag.Enumerated); + + public ReadOnlyMemory<byte> GetEnumeratedBytes(Asn1Tag expectedTag) + { + // T-REC-X.690-201508 sec 8.4 says the contents are the same as for integers. + ReadOnlyMemory<byte> contents = + GetIntegerContents(expectedTag, UniversalTagNumber.Enumerated, out int headerLength); + + _data = _data.Slice(headerLength + contents.Length); + return contents; + } + + public TEnum GetEnumeratedValue<TEnum>() where TEnum : struct + { + Type tEnum = typeof(TEnum); + + return (TEnum)Enum.ToObject(tEnum, GetEnumeratedValue(tEnum)); + } + + public TEnum GetEnumeratedValue<TEnum>(Asn1Tag expectedTag) where TEnum : struct + { + Type tEnum = typeof(TEnum); + + return (TEnum)Enum.ToObject(tEnum, GetEnumeratedValue(expectedTag, tEnum)); + } + + public Enum GetEnumeratedValue(Type tEnum) => + GetEnumeratedValue(Asn1Tag.Enumerated, tEnum); + + public Enum GetEnumeratedValue(Asn1Tag expectedTag, Type tEnum) + { + const UniversalTagNumber tagNumber = UniversalTagNumber.Enumerated; + + // This will throw an ArgumentException if TEnum isn't an enum type, + // so we don't need to validate it. + Type backingType = tEnum.GetEnumUnderlyingType(); + + if (tEnum.IsDefined(typeof(FlagsAttribute), false)) + { + throw new ArgumentException( + SR.Cryptography_Asn_EnumeratedValueRequiresNonFlagsEnum, + nameof(tEnum)); + } + + // T-REC-X.690-201508 sec 8.4 says the contents are the same as for integers. + int sizeLimit = Marshal.SizeOf(backingType); + + if (backingType == typeof(int) || + backingType == typeof(long) || + backingType == typeof(short) || + backingType == typeof(sbyte)) + { + if (!TryReadSignedInteger(sizeLimit, expectedTag, tagNumber, out long value)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return (Enum)Enum.ToObject(tEnum, value); + } + + if (backingType == typeof(uint) || + backingType == typeof(ulong) || + backingType == typeof(ushort) || + backingType == typeof(byte)) + { + if (!TryReadUnsignedInteger(sizeLimit, expectedTag, tagNumber, out ulong value)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return (Enum)Enum.ToObject(tEnum, value); + } + + Debug.Fail($"No handler for type {backingType.Name}"); + throw new CryptographicException(); + } + + private bool TryGetPrimitiveOctetStringBytes( + Asn1Tag expectedTag, + out Asn1Tag actualTag, + out int? contentLength, + out int headerLength, + out ReadOnlyMemory<byte> contents, + UniversalTagNumber universalTagNumber = UniversalTagNumber.OctetString) + { + actualTag = ReadTagAndLength(out contentLength, out headerLength); + CheckExpectedTag(actualTag, expectedTag, universalTagNumber); + + if (actualTag.IsConstructed) + { + if (_ruleSet == AsnEncodingRules.DER) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + contents = default(ReadOnlyMemory<byte>); + return false; + } + + Debug.Assert(contentLength.HasValue); + ReadOnlyMemory<byte> encodedValue = Slice(_data, headerLength, contentLength.Value); + + if (_ruleSet == AsnEncodingRules.CER && encodedValue.Length > MaxCERSegmentSize) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + contents = encodedValue; + return true; + } + + private bool TryGetPrimitiveOctetStringBytes( + Asn1Tag expectedTag, + UniversalTagNumber universalTagNumber, + out ReadOnlyMemory<byte> contents) + { + if (TryGetPrimitiveOctetStringBytes(expectedTag, out _, out _, out int headerLength, out contents, universalTagNumber)) + { + _data = _data.Slice(headerLength + contents.Length); + return true; + } + + return false; + } + + public bool TryGetPrimitiveOctetStringBytes(out ReadOnlyMemory<byte> contents) => + TryGetPrimitiveOctetStringBytes(Asn1Tag.PrimitiveOctetString, out contents); + + /// <summary> + /// Gets the contents for an octet string under a primitive encoding. + /// </summary> + /// <param name="expectedTag">The expected tag value</param> + /// <param name="contents">The contents for the octet string.</param> + /// <returns> + /// <c>true</c> if the octet string uses a primitive encoding, <c>false</c> otherwise. + /// </returns> + /// <exception cref="CryptographicException"> + /// <ul> + /// <li>No data remains</li> + /// <li>The tag read did not match the expected tag</li> + /// <li>The length is invalid under the chosen encoding rules</li> + /// <li>A CER encoding was chosen and the primitive content length exceeds the maximum allowed</li> + /// </ul> + /// </exception> + public bool TryGetPrimitiveOctetStringBytes(Asn1Tag expectedTag, out ReadOnlyMemory<byte> contents) + { + return TryGetPrimitiveOctetStringBytes(expectedTag, UniversalTagNumber.OctetString, out contents); + } + + private int CountConstructedOctetString(ReadOnlyMemory<byte> source, bool isIndefinite) + { + int contentLength = CopyConstructedOctetString( + source, + Span<byte>.Empty, + false, + isIndefinite, + out _); + + // T-REC-X.690-201508 sec 9.2 + if (_ruleSet == AsnEncodingRules.CER && contentLength <= MaxCERSegmentSize) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return contentLength; + } + + private void CopyConstructedOctetString( + ReadOnlyMemory<byte> source, + Span<byte> destination, + bool isIndefinite, + out int bytesRead, + out int bytesWritten) + { + bytesWritten = CopyConstructedOctetString( + source, + destination, + true, + isIndefinite, + out bytesRead); + } + + private int CopyConstructedOctetString( + ReadOnlyMemory<byte> source, + Span<byte> destination, + bool write, + bool isIndefinite, + out int bytesRead) + { + bytesRead = 0; + int lastSegmentLength = MaxCERSegmentSize; + + AsnReader tmpReader = new AsnReader(source, _ruleSet); + Stack<(AsnReader, bool, int)> readerStack = null; + int totalLength = 0; + Asn1Tag tag = Asn1Tag.ConstructedBitString; + Span<byte> curDest = destination; + + do + { + while (tmpReader.HasData) + { + tag = tmpReader.ReadTagAndLength(out int? length, out int headerLength); + + if (tag == Asn1Tag.PrimitiveOctetString) + { + if (_ruleSet == AsnEncodingRules.CER && lastSegmentLength != MaxCERSegmentSize) + { + // T-REC-X.690-201508 sec 9.2 + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + Debug.Assert(length != null); + + // The call to Slice here sanity checks the data bounds, length.Value is not + // reliable unless this call has succeeded. + ReadOnlyMemory<byte> contents = Slice(tmpReader._data, headerLength, length.Value); + + int localLen = headerLength + contents.Length; + tmpReader._data = tmpReader._data.Slice(localLen); + + bytesRead += localLen; + totalLength += contents.Length; + lastSegmentLength = contents.Length; + + if (_ruleSet == AsnEncodingRules.CER && lastSegmentLength > MaxCERSegmentSize) + { + // T-REC-X.690-201508 sec 9.2 + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (write) + { + contents.Span.CopyTo(curDest); + curDest = curDest.Slice(contents.Length); + } + } + else if (tag == Asn1Tag.EndOfContents && isIndefinite) + { + ValidateEndOfContents(tag, length, headerLength); + + bytesRead += headerLength; + + if (readerStack?.Count > 0) + { + (AsnReader topReader, bool wasIndefinite, int pushedBytesRead) = readerStack.Pop(); + topReader._data = topReader._data.Slice(bytesRead); + + bytesRead += pushedBytesRead; + isIndefinite = wasIndefinite; + tmpReader = topReader; + } + else + { + // We have matched the EndOfContents that brought us here. + break; + } + } + else if (tag == Asn1Tag.ConstructedOctetString) + { + if (_ruleSet == AsnEncodingRules.CER) + { + // T-REC-X.690-201508 sec 9.2 + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (readerStack == null) + { + readerStack = new Stack<(AsnReader, bool, int)>(); + } + + readerStack.Push((tmpReader, isIndefinite, bytesRead)); + + tmpReader = new AsnReader( + Slice(tmpReader._data, headerLength, length), + _ruleSet); + + bytesRead = headerLength; + isIndefinite = (length == null); + } + else + { + // T-REC-X.690-201508 sec 8.6.4.1 (in particular, Note 2) + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + if (isIndefinite && tag != Asn1Tag.EndOfContents) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (readerStack?.Count > 0) + { + (AsnReader topReader, bool wasIndefinite, int pushedBytesRead) = readerStack.Pop(); + + tmpReader = topReader; + tmpReader._data = tmpReader._data.Slice(bytesRead); + + isIndefinite = wasIndefinite; + bytesRead += pushedBytesRead; + } + else + { + tmpReader = null; + } + } while (tmpReader != null); + + return totalLength; + } + + private bool TryCopyConstructedOctetStringContents( + ReadOnlyMemory<byte> source, + Span<byte> dest, + bool isIndefinite, + out int bytesRead, + out int bytesWritten) + { + bytesRead = 0; + + int contentLength = CountConstructedOctetString(source, isIndefinite); + + if (dest.Length < contentLength) + { + bytesWritten = 0; + return false; + } + + CopyConstructedOctetString(source, dest, isIndefinite, out bytesRead, out bytesWritten); + + Debug.Assert(bytesWritten == contentLength); + return true; + } + + public bool TryCopyOctetStringBytes( + Span<byte> destination, + out int bytesWritten) + { + return TryCopyOctetStringBytes( + Asn1Tag.PrimitiveOctetString, + destination, + out bytesWritten); + } + + public bool TryCopyOctetStringBytes( + Asn1Tag expectedTag, + Span<byte> destination, + out int bytesWritten) + { + if (TryGetPrimitiveOctetStringBytes( + expectedTag, + out Asn1Tag actualTag, + out int? contentLength, + out int headerLength, + out ReadOnlyMemory<byte> contents)) + { + if (contents.Length > destination.Length) + { + bytesWritten = 0; + return false; + } + + contents.Span.CopyTo(destination); + bytesWritten = contents.Length; + _data = _data.Slice(headerLength + contents.Length); + return true; + } + + Debug.Assert(actualTag.IsConstructed); + + bool copied = TryCopyConstructedOctetStringContents( + Slice(_data, headerLength, contentLength), + destination, + contentLength == null, + out int bytesRead, + out bytesWritten); + + if (copied) + { + _data = _data.Slice(headerLength + bytesRead); + } + + return copied; + } + + public void ReadNull() => ReadNull(Asn1Tag.Null); + + public void ReadNull(Asn1Tag expectedTag) + { + Asn1Tag tag = ReadTagAndLength(out int? length, out int headerLength); + CheckExpectedTag(tag, expectedTag, UniversalTagNumber.Null); + + // T-REC-X.690-201508 sec 8.8.1 + // T-REC-X.690-201508 sec 8.8.2 + if (tag.IsConstructed || length != 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + _data = _data.Slice(headerLength); + } + + private static void ReadSubIdentifier( + ReadOnlySpan<byte> source, + out int bytesRead, + out long? smallValue, + out BigInteger? largeValue) + { + Debug.Assert(source.Length > 0); + + // T-REC-X.690-201508 sec 8.19.2 (last sentence) + if (source[0] == 0x80) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // First, see how long the segment is + int end = -1; + int idx; + + for (idx = 0; idx < source.Length; idx++) + { + // If the high bit isn't set this marks the end of the sub-identifier. + bool endOfIdentifier = (source[idx] & 0x80) == 0; + + if (endOfIdentifier) + { + end = idx; + break; + } + } + + if (end < 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + bytesRead = end + 1; + long accum = 0; + + // Fast path, 9 or fewer bytes => fits in a signed long. + // (7 semantic bits per byte * 9 bytes = 63 bits, which leaves the sign bit alone) + if (bytesRead <= 9) + { + for (idx = 0; idx < bytesRead; idx++) + { + byte cur = source[idx]; + accum <<= 7; + accum |= (byte)(cur & 0x7F); + } + + largeValue = null; + smallValue = accum; + return; + } + + // Slow path, needs temporary storage. + + const int SemanticByteCount = 7; + const int ContentByteCount = 8; + + // Every 8 content bytes turns into 7 integer bytes, so scale the count appropriately. + // Add one while we're shrunk to account for the needed padding byte or the len%8 discarded bytes. + int bytesRequired = ((bytesRead / ContentByteCount) + 1) * SemanticByteCount; + byte[] tmpBytes = ArrayPool<byte>.Shared.Rent(bytesRequired); + // Ensure all the bytes are zeroed out for BigInteger's parsing. + Array.Clear(tmpBytes, 0, tmpBytes.Length); + + Span<byte> writeSpan = tmpBytes; + Span<byte> accumValueBytes = stackalloc byte[sizeof(long)]; + int nextStop = bytesRead; + idx = bytesRead - ContentByteCount; + + while (nextStop > 0) + { + byte cur = source[idx]; + + accum <<= 7; + accum |= (byte)(cur & 0x7F); + + idx++; + + if (idx >= nextStop) + { + Debug.Assert(idx == nextStop); + Debug.Assert(writeSpan.Length >= SemanticByteCount); + + BinaryPrimitives.WriteInt64LittleEndian(accumValueBytes, accum); + Debug.Assert(accumValueBytes[7] == 0); + accumValueBytes.Slice(0, SemanticByteCount).CopyTo(writeSpan); + writeSpan = writeSpan.Slice(SemanticByteCount); + + accum = 0; + nextStop -= ContentByteCount; + idx = Math.Max(0, nextStop - ContentByteCount); + } + } + + int bytesWritten = tmpBytes.Length - writeSpan.Length; + + // Verify our bytesRequired calculation. There should be at most 7 padding bytes. + // If the length % 8 is 7 we'll have 0 padding bytes, but the sign bit is still clear. + // + // 8 content bytes had a sign bit problem, so we gave it a second 7-byte block, 7 remain. + // 7 content bytes got a single block but used and wrote 7 bytes, but only 49 of the 56 bits. + // 6 content bytes have a padding count of 1. + // 1 content byte has a padding count of 6. + // 0 content bytes is illegal, but see 8 for the cycle. + int paddingByteCount = bytesRequired - bytesWritten; + Debug.Assert(paddingByteCount >= 0 && paddingByteCount < sizeof(long)); + + largeValue = new BigInteger(tmpBytes); + smallValue = null; + + Array.Clear(tmpBytes, 0, bytesWritten); + ArrayPool<byte>.Shared.Return(tmpBytes); + } + + private string ReadObjectIdentifierAsString(Asn1Tag expectedTag, out int totalBytesRead) + { + Asn1Tag tag = ReadTagAndLength(out int? length, out int headerLength); + CheckExpectedTag(tag, expectedTag, UniversalTagNumber.ObjectIdentifier); + + // T-REC-X.690-201508 sec 8.19.1 + // T-REC-X.690-201508 sec 8.19.2 says the minimum length is 1 + if (tag.IsConstructed || length < 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ReadOnlyMemory<byte> contentsMemory = Slice(_data, headerLength, length.Value); + ReadOnlySpan<byte> contents = contentsMemory.Span; + + // Each byte can contribute a 3 digit value and a '.' (e.g. "126."), but usually + // they convey one digit and a separator. + // + // The OID with the most arcs which were found after a 30 minute search is + // "1.3.6.1.4.1.311.60.2.1.1" (EV cert jurisdiction of incorporation - locality) + // which has 11 arcs. + // The longest "known" segment is 16 bytes, a UUID-as-an-arc value. + // 16 * 11 = 176 bytes for an "extremely long" OID. + // + // So pre-allocate the StringBuilder with at most 1020 characters, an input longer than + // 255 encoded bytes will just have to re-allocate. + StringBuilder builder = new StringBuilder(((byte)contents.Length) * 4); + + ReadSubIdentifier(contents, out int bytesRead, out long? smallValue, out BigInteger? largeValue); + + // T-REC-X.690-201508 sec 8.19.4 + // The first two subidentifiers (X.Y) are encoded as (X * 40) + Y, because Y is + // bounded [0, 39] for X in {0, 1}, and only X in {0, 1, 2} are legal. + // So: + // * identifier < 40 => X = 0, Y = identifier. + // * identifier < 80 => X = 1, Y = identifier - 40. + // * else: X = 2, Y = identifier - 80. + byte firstArc; + + if (smallValue != null) + { + long firstIdentifier = smallValue.Value; + + if (firstIdentifier < 40) + { + firstArc = 0; + } + else if (firstIdentifier < 80) + { + firstArc = 1; + firstIdentifier -= 40; + } + else + { + firstArc = 2; + firstIdentifier -= 80; + } + + builder.Append(firstArc); + builder.Append('.'); + builder.Append(firstIdentifier); + } + else + { + Debug.Assert(largeValue != null); + BigInteger firstIdentifier = largeValue.Value; + + // We're only here because we were bigger than long.MaxValue, so + // we're definitely on arc 2. + Debug.Assert(firstIdentifier > long.MaxValue); + + firstArc = 2; + firstIdentifier -= 80; + + builder.Append(firstArc); + builder.Append('.'); + builder.Append(firstIdentifier.ToString()); + } + + contents = contents.Slice(bytesRead); + + while (!contents.IsEmpty) + { + ReadSubIdentifier(contents, out bytesRead, out smallValue, out largeValue); + // Exactly one should be non-null. + Debug.Assert((smallValue == null) != (largeValue == null)); + + builder.Append('.'); + + if (smallValue != null) + { + builder.Append(smallValue.Value); + } + else + { + builder.Append(largeValue.Value.ToString()); + } + + contents = contents.Slice(bytesRead); + } + + totalBytesRead = headerLength + length.Value; + return builder.ToString(); + } + + public string ReadObjectIdentifierAsString() => + ReadObjectIdentifierAsString(Asn1Tag.ObjectIdentifier); + + public string ReadObjectIdentifierAsString(Asn1Tag expectedTag) + { + string oidValue = ReadObjectIdentifierAsString(expectedTag, out int bytesRead); + + _data = _data.Slice(bytesRead); + + return oidValue; + } + + public Oid ReadObjectIdentifier(bool skipFriendlyName = false) => + ReadObjectIdentifier(Asn1Tag.ObjectIdentifier, skipFriendlyName); + + public Oid ReadObjectIdentifier(Asn1Tag expectedTag, bool skipFriendlyName=false) + { + string oidValue = ReadObjectIdentifierAsString(expectedTag, out int bytesRead); + Oid oid = skipFriendlyName ? new Oid(oidValue, oidValue) : new Oid(oidValue); + + // Don't slice until the return object has been created. + _data = _data.Slice(bytesRead); + + return oid; + } + + // T-REC-X.690-201508 sec 8.23 + private bool TryCopyCharacterStringBytes( + Asn1Tag expectedTag, + UniversalTagNumber universalTagNumber, + Span<byte> destination, + out int bytesRead, + out int bytesWritten) + { + // T-REC-X.690-201508 sec 8.23.3, all character strings are encoded as octet strings. + if (TryGetPrimitiveOctetStringBytes( + expectedTag, + out Asn1Tag actualTag, + out int? contentLength, + out int headerLength, + out ReadOnlyMemory<byte> contents, + universalTagNumber)) + { + bytesWritten = contents.Length; + + if (destination.Length < bytesWritten) + { + bytesWritten = 0; + bytesRead = 0; + return false; + } + + contents.Span.CopyTo(destination); + bytesRead = headerLength + bytesWritten; + return true; + } + + Debug.Assert(actualTag.IsConstructed); + + bool copied = TryCopyConstructedOctetStringContents( + Slice(_data, headerLength, contentLength), + destination, + contentLength == null, + out int contentBytesRead, + out bytesWritten); + + if (copied) + { + bytesRead = headerLength + contentBytesRead; + } + else + { + bytesRead = 0; + } + + return copied; + } + + private static unsafe bool TryCopyCharacterString( + ReadOnlySpan<byte> source, + Span<char> destination, + Text.Encoding encoding, + out int charsWritten) + { + if (source.Length == 0) + { + charsWritten = 0; + return true; + } + + fixed (byte* bytePtr = &source.DangerousGetPinnableReference()) + fixed (char* charPtr = &destination.DangerousGetPinnableReference()) + { + try + { + int charCount = encoding.GetCharCount(bytePtr, source.Length); + + if (charCount > destination.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = encoding.GetChars(bytePtr, source.Length, charPtr, destination.Length); + Debug.Assert(charCount == charsWritten); + } + catch (DecoderFallbackException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + + return true; + } + } + + private string GetCharacterString( + Asn1Tag expectedTag, + UniversalTagNumber universalTagNumber, + Text.Encoding encoding) + { + byte[] rented = null; + + // T-REC-X.690-201508 sec 8.23.3, all character strings are encoded as octet strings. + ReadOnlySpan<byte> contents = GetOctetStringContents( + expectedTag, + universalTagNumber, + out int bytesRead, + ref rented); + + try + { + string str; + + if (contents.Length == 0) + { + str = string.Empty; + } + else + { + unsafe + { + fixed (byte* bytePtr = &contents.DangerousGetPinnableReference()) + { + try + { + str = encoding.GetString(bytePtr, contents.Length); + } + catch (DecoderFallbackException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + } + } + } + + _data = _data.Slice(bytesRead); + return str; + } + finally + { + if (rented != null) + { + Array.Clear(rented, 0, contents.Length); + ArrayPool<byte>.Shared.Return(rented); + } + } + } + + private bool TryCopyCharacterString( + Asn1Tag expectedTag, + UniversalTagNumber universalTagNumber, + Text.Encoding encoding, + Span<char> destination, + out int charsWritten) + { + byte[] rented = null; + + // T-REC-X.690-201508 sec 8.23.3, all character strings are encoded as octet strings. + ReadOnlySpan<byte> contents = GetOctetStringContents( + expectedTag, + universalTagNumber, + out int bytesRead, + ref rented); + + try + { + bool copied = TryCopyCharacterString( + contents, + destination, + encoding, + out charsWritten); + + if (copied) + { + _data = _data.Slice(bytesRead); + } + + return copied; + } + finally + { + if (rented != null) + { + Array.Clear(rented, 0, contents.Length); + ArrayPool<byte>.Shared.Return(rented); + } + } + } + + /// <summary> + /// Gets the source data for a character string under a primitive encoding and tagged as + /// the universal class tag for the encoding type. + /// </summary> + /// <param name="encodingType">The UniversalTagNumber for the string encoding type.</param> + /// <param name="contents">The content bytes for the UTF8String payload.</param> + /// <returns> + /// <c>true</c> if the character string uses a primitive encoding, <c>false</c> otherwise. + /// </returns> + /// <exception cref="CryptographicException"> + /// <ul> + /// <li>No data remains</li> + /// <li>The tag read does not match the expected tag</li> + /// <li>The length is invalid under the chosen encoding rules</li> + /// <li>A CER encoding was chosen and the primitive content length exceeds the maximum allowed</li> + /// </ul> + /// </exception> + public bool TryGetPrimitiveCharacterStringBytes(UniversalTagNumber encodingType, out ReadOnlyMemory<byte> contents) + { + return TryGetPrimitiveCharacterStringBytes(new Asn1Tag(encodingType), encodingType, out contents); + } + + /// <summary> + /// Gets the uninterpreted contents for a character string under a primitive encoding. + /// The contents are not validated as belonging to the requested encoding type. + /// </summary> + /// <param name="expectedTag">The expected tag</param> + /// <param name="encodingType">The UniversalTagNumber for the string encoding type.</param> + /// <param name="contents">The contents for the character string.</param> + /// <returns> + /// <c>true</c> if the character string uses a primitive encoding, <c>false</c> otherwise. + /// </returns> + /// <exception cref="CryptographicException"> + /// <ul> + /// <li>No data remains</li> + /// <li>The tag read does not match the expected tag</li> + /// <li>The length is invalid under the chosen encoding rules</li> + /// <li>A CER encoding was chosen and the primitive content length exceeds the maximum allowed</li> + /// </ul> + /// </exception> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="encodingType"/> is not a known character string encoding type. + /// </exception> + public bool TryGetPrimitiveCharacterStringBytes( + Asn1Tag expectedTag, + UniversalTagNumber encodingType, + out ReadOnlyMemory<byte> contents) + { + CheckCharacterStringEncodingType(encodingType); + + // T-REC-X.690-201508 sec 8.23.3, all character strings are encoded as octet strings. + return TryGetPrimitiveOctetStringBytes(expectedTag, encodingType, out contents); + } + + public bool TryCopyCharacterStringBytes( + UniversalTagNumber encodingType, + Span<byte> destination, + out int bytesWritten) + { + return TryCopyCharacterStringBytes( + new Asn1Tag(encodingType), + encodingType, + destination, + out bytesWritten); + } + + public bool TryCopyCharacterStringBytes( + Asn1Tag expectedTag, + UniversalTagNumber encodingType, + Span<byte> destination, + out int bytesWritten) + { + CheckCharacterStringEncodingType(encodingType); + + bool copied = TryCopyCharacterStringBytes( + expectedTag, + encodingType, + destination, + out int bytesRead, + out bytesWritten); + + if (copied) + { + _data = _data.Slice(bytesRead); + } + + return copied; + } + + public bool TryCopyCharacterString( + UniversalTagNumber encodingType, + Span<char> destination, + out int charsWritten) + { + return TryCopyCharacterString( + new Asn1Tag(encodingType), + encodingType, + destination, + out charsWritten); + } + + public bool TryCopyCharacterString( + Asn1Tag expectedTag, + UniversalTagNumber encodingType, + Span<char> destination, + out int charsWritten) + { + Text.Encoding encoding = AsnCharacterStringEncodings.GetEncoding(encodingType); + return TryCopyCharacterString(expectedTag, encodingType, encoding, destination, out charsWritten); + } + + public string GetCharacterString(UniversalTagNumber encodingType) => + GetCharacterString(new Asn1Tag(encodingType), encodingType); + + public string GetCharacterString(Asn1Tag expectedTag, UniversalTagNumber encodingType) + { + Text.Encoding encoding = AsnCharacterStringEncodings.GetEncoding(encodingType); + return GetCharacterString(expectedTag, encodingType, encoding); + } + + public AsnReader ReadSequence() => ReadSequence(Asn1Tag.Sequence); + + public AsnReader ReadSequence(Asn1Tag expectedTag) + { + Asn1Tag tag = ReadTagAndLength(out int? length, out int headerLength); + CheckExpectedTag(tag, expectedTag, UniversalTagNumber.Sequence); + + // T-REC-X.690-201508 sec 8.9.1 + // T-REC-X.690-201508 sec 8.10.1 + if (!tag.IsConstructed) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + int suffix = 0; + + if (length == null) + { + length = SeekEndOfContents(_data.Slice(headerLength)); + suffix = EndOfContentsEncodedLength; + } + + ReadOnlyMemory<byte> contents = Slice(_data, headerLength, length.Value); + + _data = _data.Slice(headerLength + contents.Length + suffix); + return new AsnReader(contents, _ruleSet); + } + + /// <summary> + /// Builds a new AsnReader over the bytes bounded by the current position which + /// corresponds to an ASN.1 SET OF value, validating the CER or DER sort ordering + /// unless suppressed. + /// </summary> + /// <param name="skipSortOrderValidation"> + /// <c>false</c> to validate the sort ordering of the contents, <c>true</c> to + /// allow reading the data without verifying it was properly sorted by the writer. + /// </param> + /// <returns>An AsnReader over the current position, bounded by the contained length value.</returns> + public AsnReader ReadSetOf(bool skipSortOrderValidation = false) => + ReadSetOf(Asn1Tag.SetOf, skipSortOrderValidation); + + public AsnReader ReadSetOf(Asn1Tag expectedTag, bool skipSortOrderValidation = false) + { + Asn1Tag tag = ReadTagAndLength(out int? length, out int headerLength); + CheckExpectedTag(tag, expectedTag, UniversalTagNumber.SetOf); + + // T-REC-X.690-201508 sec 8.12.1 + if (!tag.IsConstructed) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + int suffix = 0; + + if (length == null) + { + length = SeekEndOfContents(_data.Slice(headerLength)); + suffix = EndOfContentsEncodedLength; + } + + ReadOnlyMemory<byte> contents = Slice(_data, headerLength, length.Value); + + if (!skipSortOrderValidation) + { + // T-REC-X.690-201508 sec 11.6 + // BER data is not required to be sorted. + if (_ruleSet == AsnEncodingRules.DER || + _ruleSet == AsnEncodingRules.CER) + { + AsnReader reader = new AsnReader(contents, _ruleSet); + ReadOnlyMemory<byte> current = ReadOnlyMemory<byte>.Empty; + SetOfValueComparer comparer = SetOfValueComparer.Instance; + + while (reader.HasData) + { + ReadOnlyMemory<byte> previous = current; + current = reader.GetEncodedValue(); + + if (comparer.Compare(current, previous) < 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + } + } + + _data = _data.Slice(headerLength + contents.Length + suffix); + return new AsnReader(contents, _ruleSet); + } + + private static int ParseNonNegativeIntAndSlice(ref ReadOnlySpan<byte> data, int bytesToRead) + { + int value = ParseNonNegativeInt(Slice(data, 0, bytesToRead)); + data = data.Slice(bytesToRead); + + return value; + } + + private static int ParseNonNegativeInt(ReadOnlySpan<byte> data) + { + if (Utf8Parser.TryParse(data, out uint value, out int consumed) && value <= int.MaxValue && consumed == data.Length) + { + return (int)value; + } + + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + private DateTimeOffset ParseUtcTime(ReadOnlySpan<byte> contentOctets, int twoDigitYearMax) + { + // The full allowed formats (T-REC-X.680-201510 sec 47.3) + // a) YYMMDD + // b1) hhmm + // b2) hhmmss + // c1) Z + // c2) {+|-}hhmm + // + // YYMMDDhhmmZ (a, b1, c1) + // YYMMDDhhmm+hhmm (a, b1, c2+) + // YYMMDDhhmm-hhmm (a, b1, c2-) + // YYMMDDhhmmssZ (a, b2, c1) + // YYMMDDhhmmss+hhmm (a, b2, c2+) + // YYMMDDhhmmss-hhmm (a, b2, c2-) + + const int NoSecondsZulu = 11; + const int NoSecondsOffset = 15; + const int HasSecondsZulu = 13; + const int HasSecondsOffset = 17; + + // T-REC-X.690-201510 sec 11.8 + if (_ruleSet == AsnEncodingRules.DER || _ruleSet == AsnEncodingRules.CER) + { + if (contentOctets.Length != HasSecondsZulu) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + // 11, 13, 15, 17 are legal. + // Range check + odd. + if (contentOctets.Length < NoSecondsZulu || + contentOctets.Length > HasSecondsOffset || + (contentOctets.Length & 1) != 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ReadOnlySpan<byte> contents = contentOctets; + + int year = ParseNonNegativeIntAndSlice(ref contents, 2); + int month = ParseNonNegativeIntAndSlice(ref contents, 2); + int day = ParseNonNegativeIntAndSlice(ref contents, 2); + int hour = ParseNonNegativeIntAndSlice(ref contents, 2); + int minute = ParseNonNegativeIntAndSlice(ref contents, 2); + int second = 0; + int offsetHour = 0; + int offsetMinute = 0; + bool minus = false; + + if (contentOctets.Length == HasSecondsOffset || + contentOctets.Length == HasSecondsZulu) + { + second = ParseNonNegativeIntAndSlice(ref contents, 2); + } + + if (contentOctets.Length == NoSecondsZulu || + contentOctets.Length == HasSecondsZulu) + { + if (contents[0] != 'Z') + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + else + { + Debug.Assert( + contentOctets.Length == NoSecondsOffset || + contentOctets.Length == HasSecondsOffset); + + if (contents[0] == '-') + { + minus = true; + } + else if (contents[0] != '+') + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + contents = contents.Slice(1); + offsetHour = ParseNonNegativeIntAndSlice(ref contents, 2); + offsetMinute = ParseNonNegativeIntAndSlice(ref contents, 2); + Debug.Assert(contents.IsEmpty); + } + + // ISO 8601:2004 4.2.1 restricts a "minute" value to [00,59]. + // The "hour" value is effectively bound to [00,23] by the same section, but + // is bound to [00,14] by DateTimeOffset, so no additional check is required here. + if (offsetMinute > 59) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + TimeSpan offset = new TimeSpan(offsetHour, offsetMinute, 0); + + if (minus) + { + offset = -offset; + } + + // Apply the twoDigitYearMax value. + // Example: year=50, TDYM=2049 + // century = 20 + // year > 49 => century = 19 + // scaledYear = 1900 + 50 = 1950 + // + // Example: year=49, TDYM=2049 + // century = 20 + // year is not > 49 => century = 20 + // scaledYear = 2000 + 49 = 2049 + int century = twoDigitYearMax / 100; + + if (year > twoDigitYearMax % 100) + { + century--; + } + + int scaledYear = century * 100 + year; + + try + { + return new DateTimeOffset(scaledYear, month, day, hour, minute, second, offset); + } + catch (Exception e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + } + + public DateTimeOffset GetUtcTime(int twoDigitYearMax = 2049) => + GetUtcTime(Asn1Tag.UtcTime, twoDigitYearMax); + + /// <summary> + /// Gets the DateTimeOffset represented by a UTCTime value. + /// </summary> + /// <param name="expectedTag">The expected tag</param> + /// <param name="twoDigitYearMax"> + /// The largest year to represent with this value. + /// The default value, 2049, represents the 1950-2049 range for X.509 certificates. + /// </param> + /// <returns> + /// A DateTimeOffset representing the value encoded in the UTCTime. + /// </returns> + /// <seealso cref="System.Globalization.Calendar.TwoDigitYearMax"/> + public DateTimeOffset GetUtcTime(Asn1Tag expectedTag, int twoDigitYearMax = 2049) + { + // T-REC-X.680-201510 sec 47.3 says it is IMPLICIT VisibleString, which means + // that BER is allowed to do complex constructed forms. + + // The full allowed formats (T-REC-X.680-201510 sec 47.3) + // YYMMDDhhmmZ (a, b1, c1) + // YYMMDDhhmm+hhmm (a, b1, c2+) + // YYMMDDhhmm-hhmm (a, b1, c2-) + // YYMMDDhhmmssZ (a, b2, c1) + // YYMMDDhhmmss+hhmm (a, b2, c2+) + // YYMMDDhhmmss-hhmm (a, b2, c2-) + + // CER and DER are restricted to YYMMDDhhmmssZ + // T-REC-X.690-201510 sec 11.8 + + byte[] rented = null; + // The longest format is 17 bytes. + Span<byte> tmpSpace = stackalloc byte[17]; + + ReadOnlySpan<byte> contents = GetOctetStringContents( + expectedTag, + UniversalTagNumber.UtcTime, + out int bytesRead, + ref rented, + tmpSpace); + + DateTimeOffset value = ParseUtcTime(contents, twoDigitYearMax); + + if (rented != null) + { + Debug.Fail($"UtcTime did not fit in tmpSpace ({contents.Length} total)"); + Array.Clear(rented, 0, contents.Length); + ArrayPool<byte>.Shared.Return(rented); + } + + _data = _data.Slice(bytesRead); + return value; + } + + private static DateTimeOffset ParseGeneralizedTime( + AsnEncodingRules ruleSet, + ReadOnlySpan<byte> contentOctets, + bool disallowFractions) + { + // T-REC-X.680-201510 sec 46 defines a lot of formats for GeneralizedTime. + // + // All formats start with yyyyMMdd. + // + // "Local time" formats are + // [date]HH.fractionOfAnHourToAnArbitraryPrecision + // [date]HHmm.fractionOfAMinuteToAnArbitraryPrecision + // [date]HHmmss.fractionOfASecondToAnArbitraryPrecision + // + // "UTC time" formats are the local formats suffixed with 'Z' + // + // "UTC offset time" formats are the local formats suffixed with + // +HH + // +HHmm + // -HH + // -HHmm + // + // Since T-REC-X.680-201510 46.3(a)(1) and 46.3(a)(2) both specify the ISO 8601:2004 + // Basic format, we shall presume that 46.3(a)(3) also meant only the Basic format, + // and therefore [+/-]HH:mm (with the colon) are prohibited. (based on ISO 8601:201x-DIS) + + // Since DateTimeOffset doesn't have a notion of + // "I'm a local time, but with an unknown offset", the computer's current offset will + // be used. + + // T-REC-X.690-201510 sec 11.7 binds CER and DER to a much smaller set of inputs: + // * Only the UTC/Z format can be used. + // * HHmmss must always be used + // * If fractions are present they will be separated by period, never comma. + // * If fractions are present the last digit mustn't be 0. + + bool strict = ruleSet == AsnEncodingRules.DER || ruleSet == AsnEncodingRules.CER; + if (strict && contentOctets.Length < 15) + { + // yyyyMMddHHmmssZ + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + else if (contentOctets.Length < 10) + { + // yyyyMMddHH + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ReadOnlySpan<byte> contents = contentOctets; + + int year = ParseNonNegativeIntAndSlice(ref contents, 4); + int month = ParseNonNegativeIntAndSlice(ref contents, 2); + int day = ParseNonNegativeIntAndSlice(ref contents, 2); + int hour = ParseNonNegativeIntAndSlice(ref contents, 2); + int? minute = null; + int? second = null; + ulong fraction = 0; + ulong fractionScale = 1; + byte lastFracDigit = 0xFF; + TimeSpan? timeOffset = null; + bool isZulu = false; + + const byte HmsState = 0; + const byte FracState = 1; + const byte SuffixState = 2; + byte state = HmsState; + + byte? GetNextState(byte octet) + { + if (octet == 'Z' || octet == '-' || octet == '+') + { + return SuffixState; + } + + if (octet == '.' || octet == ',') + { + return FracState; + } + + return null; + } + + // This while loop could be rewritten to include the FracState and Suffix + // processing steps. But since there's a forward flow to the state machine + // the loop body then needs to account for that. + while (state == HmsState && contents.Length != 0) + { + byte? nextState = GetNextState(contents[0]); + + if (nextState == null) + { + if (minute == null) + { + minute = ParseNonNegativeIntAndSlice(ref contents, 2); + } + else if (second == null) + { + second = ParseNonNegativeIntAndSlice(ref contents, 2); + } + else + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + else + { + state = nextState.Value; + } + } + + if (state == FracState) + { + if (disallowFractions) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + Debug.Assert(!contents.IsEmpty); + byte octet = contents[0]; + Debug.Assert(state == GetNextState(octet)); + + if (octet == '.') + { + // Always valid + } + else if (octet == ',') + { + // Valid for BER, but not CER or DER. + // T-REC-X.690-201510 sec 11.7.4 + if (strict) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + else + { + Debug.Fail($"Unhandled value '{octet:X2}' in {nameof(FracState)}"); + throw new CryptographicException(); + } + + contents = contents.Slice(1); + + if (contents.IsEmpty) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // There are 36,000,000,000 ticks per hour, and hour is our largest scale. + // In case the double -> Ticks conversion allows for rounding up we can allow + // for a 12th digit. + + if (!Utf8Parser.TryParse(SliceAtMost(contents, 12), out fraction, out int fracLength) || fracLength == 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + lastFracDigit = (byte)(fraction % 10); + + for (int i = 0; i < fracLength; i++) + { + fractionScale *= 10; + } + + contents = contents.Slice(fracLength); + + // Drain off any remaining digits. + // The unsigned parsers will not accept + or - as a leading character, so + // they won't eat timezone suffix. + // But Utf8Parser.TryParse reports false on overflow, so limit it to 9 digits at a time. + while (Utf8Parser.TryParse(SliceAtMost(contents, 9), out uint nonSemantic, out fracLength)) + { + contents = contents.Slice(fracLength); + lastFracDigit = (byte)(nonSemantic % 10); + } + + if (contents.Length != 0) + { + byte? nextState = GetNextState(contents[0]); + + if (nextState == null) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // If this produces FracState we'll finish with a non-empty contents, and still throw. + state = nextState.Value; + } + } + + if (state == SuffixState) + { + Debug.Assert(!contents.IsEmpty); + byte octet = contents[0]; + Debug.Assert(state == GetNextState(octet)); + contents = contents.Slice(1); + + if (octet == 'Z') + { + timeOffset = TimeSpan.Zero; + isZulu = true; + } + else + { + bool isMinus; + + if (octet == '+') + { + isMinus = false; + } + else if (octet == '-') + { + isMinus = true; + } + else + { + Debug.Fail($"Unhandled value '{octet:X2}' in {nameof(SuffixState)}"); + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (contents.IsEmpty) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + int offsetHour = ParseNonNegativeIntAndSlice(ref contents, 2); + int offsetMinute = 0; + + if (contents.Length != 0) + { + offsetMinute = ParseNonNegativeIntAndSlice(ref contents, 2); + } + + // ISO 8601:2004 4.2.1 restricts a "minute" value to [00,59]. + // The "hour" value is effectively bound to [00,23] by the same section, but + // is bound to [00,14] by DateTimeOffset, so no additional check is required here. + if (offsetMinute > 59) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + TimeSpan tmp = new TimeSpan(offsetHour, offsetMinute, 0); + + if (isMinus) + { + tmp = -tmp; + } + + timeOffset = tmp; + } + } + + // Was there data after a suffix, or fracstate went re-entrant? + if (!contents.IsEmpty) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // T-REC-X.690-201510 sec 11.7 + if (strict) + { + if (!isZulu || !second.HasValue) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (lastFracDigit == 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + double frac = (double)fraction / fractionScale; + TimeSpan fractionSpan = TimeSpan.Zero; + + if (!minute.HasValue) + { + minute = 0; + second = 0; + + if (fraction != 0) + { + // No minutes means this is fractions of an hour + fractionSpan = TimeSpan.FromHours(frac); + } + } + else if (!second.HasValue) + { + second = 0; + + if (fraction != 0) + { + // No seconds means this is fractions of a minute + fractionSpan = TimeSpan.FromMinutes(frac); + } + } + else if (fraction != 0) + { + // Both minutes and seconds means fractions of a second. + fractionSpan = TimeSpan.FromSeconds(frac); + } + + DateTimeOffset value; + + try + { + if (timeOffset == null) + { + // Use the local timezone offset since there's no information in the contents. + // T-REC-X.680-201510 sec 46.2(a). + value = new DateTimeOffset(new DateTime(year, month, day, hour, minute.Value, second.Value)); + } + else + { + // T-REC-X.680-201510 sec 46.2(b) or 46.2(c). + value = new DateTimeOffset(year, month, day, hour, minute.Value, second.Value, timeOffset.Value); + } + + value += fractionSpan; + return value; + } + catch (Exception e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + } + + public DateTimeOffset GetGeneralizedTime(bool disallowFractions=false) => + GetGeneralizedTime(Asn1Tag.GeneralizedTime, disallowFractions); + + public DateTimeOffset GetGeneralizedTime(Asn1Tag expectedTag, bool disallowFractions=false) + { + byte[] rented = null; + + ReadOnlySpan<byte> contents = GetOctetStringContents( + expectedTag, + UniversalTagNumber.GeneralizedTime, + out int bytesRead, + ref rented); + + DateTimeOffset value = ParseGeneralizedTime(_ruleSet, contents, disallowFractions); + + if (rented != null) + { + Array.Clear(rented, 0, contents.Length); + ArrayPool<byte>.Shared.Return(rented); + } + + _data = _data.Slice(bytesRead); + return value; + } + + private ReadOnlySpan<byte> GetOctetStringContents( + Asn1Tag expectedTag, + UniversalTagNumber universalTagNumber, + out int bytesRead, + ref byte[] rented, + Span<byte> tmpSpace = default) + { + Debug.Assert(rented == null); + + if (TryGetPrimitiveOctetStringBytes( + expectedTag, + out Asn1Tag actualTag, + out int? contentLength, + out int headerLength, + out ReadOnlyMemory<byte> contentsOctets, + universalTagNumber)) + { + bytesRead = headerLength + contentsOctets.Length; + return contentsOctets.Span; + } + + Debug.Assert(actualTag.IsConstructed); + + ReadOnlyMemory<byte> source = Slice(_data, headerLength, contentLength); + bool isIndefinite = contentLength == null; + int octetStringLength = CountConstructedOctetString(source, isIndefinite); + + if (tmpSpace.Length < octetStringLength) + { + rented = ArrayPool<byte>.Shared.Rent(octetStringLength); + tmpSpace = rented; + } + + CopyConstructedOctetString(source, tmpSpace, isIndefinite, out int localBytesRead, out int bytesWritten); + Debug.Assert(bytesWritten == octetStringLength); + + bytesRead = headerLength + localBytesRead; + return tmpSpace.Slice(0, bytesWritten); + } + + private static ReadOnlySpan<byte> SliceAtMost(ReadOnlySpan<byte> source, int longestPermitted) + { + int len = Math.Min(longestPermitted, source.Length); + return source.Slice(0, len); + } + + private static ReadOnlySpan<byte> Slice(ReadOnlySpan<byte> source, int offset, int length) + { + Debug.Assert(offset >= 0); + + if (length < 0 || source.Length - offset < length) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return source.Slice(offset, length); + } + + private static ReadOnlyMemory<byte> Slice(ReadOnlyMemory<byte> source, int offset, int? length) + { + Debug.Assert(offset >= 0); + + if (length == null) + { + return source.Slice(offset); + } + + int lengthVal = length.Value; + + if (lengthVal < 0 || source.Length - offset < lengthVal) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return source.Slice(offset, lengthVal); + } + + private static void CheckEncodingRules(AsnEncodingRules ruleSet) + { + if (ruleSet != AsnEncodingRules.BER && + ruleSet != AsnEncodingRules.CER && + ruleSet != AsnEncodingRules.DER) + { + throw new ArgumentOutOfRangeException(nameof(ruleSet)); + } + } + + private static void CheckExpectedTag(Asn1Tag tag, Asn1Tag expectedTag, UniversalTagNumber tagNumber) + { + if (expectedTag.TagClass == TagClass.Universal && expectedTag.TagValue != (int)tagNumber) + { + throw new ArgumentException( + SR.Cryptography_Asn_UniversalValueIsFixed, + nameof(expectedTag)); + } + + if (expectedTag.TagClass != tag.TagClass || expectedTag.TagValue != tag.TagValue) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + private static void CheckCharacterStringEncodingType(UniversalTagNumber encodingType) + { + // T-REC-X.680-201508 sec 41 + switch (encodingType) + { + case UniversalTagNumber.BMPString: + case UniversalTagNumber.GeneralString: + case UniversalTagNumber.GraphicString: + case UniversalTagNumber.IA5String: + case UniversalTagNumber.ISO646String: + case UniversalTagNumber.NumericString: + case UniversalTagNumber.PrintableString: + case UniversalTagNumber.TeletexString: + // T61String is an alias for TeletexString (already listed) + case UniversalTagNumber.UniversalString: + case UniversalTagNumber.UTF8String: + case UniversalTagNumber.VideotexString: + // VisibleString is an alias for ISO646String (already listed) + return; + } + + throw new ArgumentOutOfRangeException(nameof(encodingType)); + } + } +} diff --git a/src/Common/src/System/Security/Cryptography/AsnWriter.cs b/src/Common/src/System/Security/Cryptography/AsnWriter.cs new file mode 100644 index 0000000000..6119537da1 --- /dev/null +++ b/src/Common/src/System/Security/Cryptography/AsnWriter.cs @@ -0,0 +1,1602 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Enable CHECK_ACCURATE_ENSURE to ensure that the AsnWriter is not ever +// abusing the normal EnsureWriteCapacity + ArrayPool behaviors of rounding up. +//#define CHECK_ACCURATE_ENSURE + +using System.Buffers; +using System.Buffers.Text; +using System.Collections.Generic; +using System.Diagnostics; +using System.Numerics; + +namespace System.Security.Cryptography.Asn1 +{ + internal class AsnWriter + { + private byte[] _buffer; + private int _offset; + private Stack<(Asn1Tag,int)> _nestingStack; + + public AsnEncodingRules RuleSet { get; } + + public AsnWriter(AsnEncodingRules ruleSet) + { + if (ruleSet != AsnEncodingRules.BER && + ruleSet != AsnEncodingRules.CER && + ruleSet != AsnEncodingRules.DER) + { + throw new ArgumentOutOfRangeException(nameof(ruleSet)); + } + + RuleSet = ruleSet; + } + + private void EnsureWriteCapacity(int pendingCount) + { + if (pendingCount < 0) + { + throw new OverflowException(); + } + + if (_buffer == null || _buffer.Length - _offset < pendingCount) + { +#if CHECK_ACCURATE_ENSURE +// A debug paradigm to make sure that throughout the execution nothing ever writes +// past where the buffer was "allocated". This causes quite a number of reallocs +// and copies, so it's a #define opt-in. + byte[] newBytes = new byte[_offset + pendingCount]; + + if (_buffer != null) + { + Buffer.BlockCopy(_buffer, 0, newBytes, 0, _offset); + } +#else + const int BlockSize = 1024; + // While the ArrayPool may have similar logic, make sure we don't run into a lot of + // "grow a little" by asking in 1k steps. + int blocks = checked(_offset + pendingCount + (BlockSize - 1)) / BlockSize; + byte[] newBytes = ArrayPool<byte>.Shared.Rent(BlockSize * blocks); + + if (_buffer != null) + { + Buffer.BlockCopy(_buffer, 0, newBytes, 0, _offset); + Array.Clear(_buffer, 0, _offset); + ArrayPool<byte>.Shared.Return(_buffer); + } +#endif + +#if DEBUG + // Ensure no "implicit 0" is happening + for (int i = _offset; i < newBytes.Length; i++) + { + newBytes[i] ^= 0xFF; + } +#endif + + _buffer = newBytes; + } + } + + private void WriteTag(Asn1Tag tag) + { + int spaceRequired = tag.CalculateEncodedSize(); + EnsureWriteCapacity(spaceRequired); + + if (!tag.TryWrite(_buffer.AsSpan().Slice(_offset, spaceRequired), out int written) || + written != spaceRequired) + { + Debug.Fail($"TryWrite failed or written was wrong value ({written} vs {spaceRequired})"); + throw new CryptographicException(); + } + + _offset += spaceRequired; + } + + // T-REC-X.690-201508 sec 8.1.3 + private void WriteLength(int length) + { + const byte MultiByteMarker = 0x80; + Debug.Assert(length >= -1); + + // If the indefinite form has been requested. + // T-REC-X.690-201508 sec 8.1.3.6 + if (length == -1) + { + EnsureWriteCapacity(1); + _buffer[_offset] = MultiByteMarker; + _offset++; + return; + } + + Debug.Assert(length >= 0); + + // T-REC-X.690-201508 sec 8.1.3.3, 8.1.3.4 + if (length < MultiByteMarker) + { + // Pre-allocate the pending data since we know how much. + EnsureWriteCapacity(1 + length); + _buffer[_offset] = (byte)length; + _offset++; + return; + } + + // The rest of the method implements T-REC-X.680-201508 sec 8.1.3.5 + int lengthLength = GetEncodedLengthSubsequentByteCount(length); + + // Pre-allocate the pending data since we know how much. + EnsureWriteCapacity(lengthLength + 1 + length); + _buffer[_offset] = (byte)(MultiByteMarker | lengthLength); + + // No minus one because offset didn't get incremented yet. + int idx = _offset + lengthLength; + + int remaining = length; + + do + { + _buffer[idx] = (byte)remaining; + remaining >>= 8; + idx--; + } while (remaining > 0); + + Debug.Assert(idx == _offset); + _offset += lengthLength + 1; + } + + // T-REC-X.690-201508 sec 8.1.3.5 + private static int GetEncodedLengthSubsequentByteCount(int length) + { + if (length <= 0x7F) + return 0; + if (length <= byte.MaxValue) + return 1; + if (length <= ushort.MaxValue) + return 2; + if (length <= 0x00FFFFFF) + return 3; + + return 4; + } + + public void WriteEncodedValue(ReadOnlyMemory<byte> preEncodedValue) + { + AsnReader reader = new AsnReader(preEncodedValue, RuleSet); + + // Is it legal under the current rules? + ReadOnlyMemory<byte> parsedBack = reader.GetEncodedValue(); + + if (reader.HasData) + { + throw new ArgumentException(SR.Cryptography_WriteEncodedValue_OneValueAtATime, nameof(preEncodedValue)); + } + + Debug.Assert(parsedBack.Length == preEncodedValue.Length); + + EnsureWriteCapacity(preEncodedValue.Length); + preEncodedValue.Span.CopyTo(_buffer.AsSpan().Slice(_offset)); + _offset += preEncodedValue.Length; + } + + // T-REC-X.690-201508 sec 8.1.5 + private void WriteEndOfContents() + { + EnsureWriteCapacity(2); + _buffer[_offset++] = 0; + _buffer[_offset++] = 0; + } + + public void WriteBoolean(bool value) + { + WriteBooleanCore(Asn1Tag.Boolean, value); + } + + public void WriteBoolean(Asn1Tag tag, bool value) + { + CheckUniversalTag(tag, UniversalTagNumber.Boolean); + + WriteBooleanCore(tag.AsPrimitive(), value); + } + + // T-REC-X.690-201508 sec 11.1, 8.2 + private void WriteBooleanCore(Asn1Tag tag, bool value) + { + Debug.Assert(!tag.IsConstructed); + WriteTag(tag); + WriteLength(1); + // Ensured by WriteLength + Debug.Assert(_offset < _buffer.Length); + _buffer[_offset] = (byte)(value ? 0xFF : 0x00); + _offset++; + } + + public void WriteInteger(long value) + { + WriteIntegerCore(Asn1Tag.Integer, value); + } + + public void WriteInteger(ulong value) + { + WriteNonNegativeIntegerCore(Asn1Tag.Integer, value); + } + + public void WriteInteger(BigInteger value) + { + WriteIntegerCore(Asn1Tag.Integer, value); + } + + public void WriteInteger(Asn1Tag tag, long value) + { + CheckUniversalTag(tag, UniversalTagNumber.Integer); + + WriteIntegerCore(tag.AsPrimitive(), value); + } + + // T-REC-X.690-201508 sec 8.3 + private void WriteIntegerCore(Asn1Tag tag, long value) + { + if (value >= 0) + { + WriteNonNegativeIntegerCore(tag, (ulong)value); + return; + } + + int valueLength; + + if (value >= sbyte.MinValue) + valueLength = 1; + else if (value >= short.MinValue) + valueLength = 2; + else if (value >= unchecked((long)0xFFFFFFFF_FF800000)) + valueLength = 3; + else if (value >= int.MinValue) + valueLength = 4; + else if (value >= unchecked((long)0xFFFFFF80_00000000)) + valueLength = 5; + else if (value >= unchecked((long)0xFFFF8000_00000000)) + valueLength = 6; + else if (value >= unchecked((long)0xFF800000_00000000)) + valueLength = 7; + else + valueLength = 8; + + Debug.Assert(!tag.IsConstructed); + WriteTag(tag); + WriteLength(valueLength); + + long remaining = value; + int idx = _offset + valueLength - 1; + + do + { + _buffer[idx] = (byte)remaining; + remaining >>= 8; + idx--; + } while (idx >= _offset); + +#if DEBUG + if (valueLength > 1) + { + // T-REC-X.690-201508 sec 8.3.2 + // Cannot start with 9 bits of 1 (or 9 bits of 0, but that's not this method). + Debug.Assert(_buffer[_offset] != 0xFF || _buffer[_offset + 1] < 0x80); + } +#endif + + _offset += valueLength; + } + + public void WriteInteger(Asn1Tag tag, ulong value) + { + CheckUniversalTag(tag, UniversalTagNumber.Integer); + + WriteNonNegativeIntegerCore(tag.AsPrimitive(), value); + } + + // T-REC-X.690-201508 sec 8.3 + private void WriteNonNegativeIntegerCore(Asn1Tag tag, ulong value) + { + int valueLength; + + // 0x80 needs two bytes: 0x00 0x80 + if (value < 0x80) + valueLength = 1; + else if (value < 0x8000) + valueLength = 2; + else if (value < 0x800000) + valueLength = 3; + else if (value < 0x80000000) + valueLength = 4; + else if (value < 0x80_00000000) + valueLength = 5; + else if (value < 0x8000_00000000) + valueLength = 6; + else if (value < 0x800000_00000000) + valueLength = 7; + else if (value < 0x80000000_00000000) + valueLength = 8; + else + valueLength = 9; + + // Clear the constructed bit, if it was set. + Debug.Assert(!tag.IsConstructed); + WriteTag(tag); + WriteLength(valueLength); + + ulong remaining = value; + int idx = _offset + valueLength - 1; + + do + { + _buffer[idx] = (byte)remaining; + remaining >>= 8; + idx--; + } while (idx >= _offset); + +#if DEBUG + if (valueLength > 1) + { + // T-REC-X.690-201508 sec 8.3.2 + // Cannot start with 9 bits of 0 (or 9 bits of 1, but that's not this method). + Debug.Assert(_buffer[_offset] != 0 || _buffer[_offset + 1] > 0x7F); + } +#endif + + _offset += valueLength; + } + + public void WriteInteger(Asn1Tag tag, BigInteger value) + { + CheckUniversalTag(tag, UniversalTagNumber.Integer); + + WriteIntegerCore(tag.AsPrimitive(), value); + } + + // T-REC-X.690-201508 sec 8.3 + private void WriteIntegerCore(Asn1Tag tag, BigInteger value) + { + // TODO: Split this for netstandard vs netcoreapp for span-perf?. + byte[] encoded = value.ToByteArray(); + Array.Reverse(encoded); + + Debug.Assert(!tag.IsConstructed); + WriteTag(tag); + WriteLength(encoded.Length); + Buffer.BlockCopy(encoded, 0, _buffer, _offset, encoded.Length); + _offset += encoded.Length; + } + + public void WriteBitString(ReadOnlySpan<byte> bitString, int unusedBitCount=0) + { + WriteBitStringCore(Asn1Tag.PrimitiveBitString, bitString, unusedBitCount); + } + + public void WriteBitString(Asn1Tag tag, ReadOnlySpan<byte> bitString, int unusedBitCount=0) + { + CheckUniversalTag(tag, UniversalTagNumber.BitString); + + // Primitive or constructed, doesn't matter. + WriteBitStringCore(tag, bitString, unusedBitCount); + } + + // T-REC-X.690-201508 sec 8.6 + private void WriteBitStringCore(Asn1Tag tag, ReadOnlySpan<byte> bitString, int unusedBitCount) + { + // T-REC-X.690-201508 sec 8.6.2.2 + if (unusedBitCount < 0 || unusedBitCount > 7) + { + throw new ArgumentOutOfRangeException( + nameof(unusedBitCount), + unusedBitCount, + SR.Cryptography_Asn_UnusedBitCountRange); + } + + // T-REC-X.690-201508 sec 8.6.2.3 + if (bitString.Length == 0 && unusedBitCount != 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // If 3 bits are "unused" then build a mask for them to check for 0. + // 1 << 3 => 0b0000_1000 + // subtract 1 => 0b000_0111 + int mask = (1 << unusedBitCount) - 1; + byte lastByte = bitString.IsEmpty ? (byte)0 : bitString[bitString.Length - 1]; + + if ((lastByte & mask) != 0) + { + // T-REC-X.690-201508 sec 11.2 + // + // This could be ignored for BER, but since DER is more common and + // it likely suggests a program error on the caller, leave it enabled for + // BER for now. + // TODO: Probably warrants a distinct message. + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (RuleSet == AsnEncodingRules.CER) + { + // T-REC-X.690-201508 sec 9.2 + // + // If it's not within a primitive segment, use the constructed encoding. + // (>= instead of > because of the unused bit count byte) + if (bitString.Length >= AsnReader.MaxCERSegmentSize) + { + WriteConstructedCerBitString(tag, bitString, unusedBitCount); + return; + } + } + + // Clear the constructed flag, if present. + WriteTag(tag.AsPrimitive()); + // The unused bits byte requires +1. + WriteLength(bitString.Length + 1); + _buffer[_offset] = (byte)unusedBitCount; + _offset++; + bitString.CopyTo(_buffer.AsSpan().Slice(_offset)); + _offset += bitString.Length; + } + + // T-REC-X.690-201508 sec 9.2, 8.6 + private void WriteConstructedCerBitString(Asn1Tag tag, ReadOnlySpan<byte> payload, int unusedBitCount) + { + const int MaxCERSegmentSize = AsnReader.MaxCERSegmentSize; + // Every segment has an "unused bit count" byte. + const int MaxCERContentSize = MaxCERSegmentSize - 1; + Debug.Assert(payload.Length > MaxCERContentSize); + + WriteTag(tag.AsConstructed()); + // T-REC-X.690-201508 sec 9.1 + // Constructed CER uses the indefinite form. + WriteLength(-1); + + int fullSegments = Math.DivRem(payload.Length, MaxCERContentSize, out int lastContentSize); + + // The tag size is 1 byte. + // The length will always be encoded as 82 03 E8 (3 bytes) + // And 1000 content octets (by T-REC-X.690-201508 sec 9.2) + const int FullSegmentEncodedSize = 1004; + Debug.Assert( + FullSegmentEncodedSize == 1 + 1 + MaxCERSegmentSize + GetEncodedLengthSubsequentByteCount(MaxCERSegmentSize)); + + int remainingEncodedSize; + + if (lastContentSize == 0) + { + remainingEncodedSize = 0; + } + else + { + // One byte of tag, minimum one byte of length, and one byte of unused bit count. + remainingEncodedSize = 3 + lastContentSize + GetEncodedLengthSubsequentByteCount(lastContentSize); + } + + // Reduce the number of copies by pre-calculating the size. + // +2 for End-Of-Contents + int expectedSize = fullSegments * FullSegmentEncodedSize + remainingEncodedSize + 2; + EnsureWriteCapacity(expectedSize); + + byte[] ensureNoExtraCopy = _buffer; + int savedOffset = _offset; + + ReadOnlySpan<byte> remainingData = payload; + Span<byte> dest; + Asn1Tag primitiveBitString = Asn1Tag.PrimitiveBitString; + + while (remainingData.Length > MaxCERContentSize) + { + // T-REC-X.690-201508 sec 8.6.4.1 + WriteTag(primitiveBitString); + WriteLength(MaxCERSegmentSize); + // 0 unused bits in this segment. + _buffer[_offset] = 0; + _offset++; + + dest = _buffer.AsSpan().Slice(_offset); + remainingData.Slice(0, MaxCERContentSize).CopyTo(dest); + + remainingData = remainingData.Slice(MaxCERContentSize); + _offset += MaxCERContentSize; + } + + WriteTag(primitiveBitString); + WriteLength(remainingData.Length + 1); + + _buffer[_offset] = (byte)unusedBitCount; + _offset++; + + dest = _buffer.AsSpan().Slice(_offset); + remainingData.CopyTo(dest); + _offset += remainingData.Length; + + WriteEndOfContents(); + + Debug.Assert(_offset - savedOffset == expectedSize, $"expected size was {expectedSize}, actual was {_offset - savedOffset}"); + Debug.Assert(_buffer == ensureNoExtraCopy, $"_buffer was replaced during {nameof(WriteConstructedCerBitString)}"); + } + + public void WriteNamedBitList(object enumValue) + { + if (enumValue == null) + throw new ArgumentNullException(nameof(enumValue)); + + WriteNamedBitList(Asn1Tag.PrimitiveBitString, enumValue); + } + + public void WriteNamedBitList<TEnum>(TEnum enumValue) where TEnum : struct + { + WriteNamedBitList(Asn1Tag.PrimitiveBitString, enumValue); + } + + public void WriteNamedBitList(Asn1Tag tag, object enumValue) + { + if (enumValue == null) + throw new ArgumentNullException(nameof(enumValue)); + + WriteNamedBitList(tag, enumValue.GetType(), enumValue); + } + + public void WriteNamedBitList<TEnum>(Asn1Tag tag, TEnum enumValue) where TEnum : struct + { + WriteNamedBitList(tag, typeof(TEnum), enumValue); + } + + private void WriteNamedBitList(Asn1Tag tag, Type tEnum, object enumValue) + { + Type backingType = tEnum.GetEnumUnderlyingType(); + + if (!tEnum.IsDefined(typeof(FlagsAttribute), false)) + { + throw new ArgumentException(SR.Cryptography_Asn_NamedBitListRequiresFlagsEnum, nameof(tEnum)); + } + + ulong integralValue; + + if (backingType == typeof(ulong)) + { + integralValue = Convert.ToUInt64(enumValue); + } + else + { + // All other types fit in a (signed) long. + long numericValue = Convert.ToInt64(enumValue); + integralValue = unchecked((ulong)numericValue); + } + + WriteNamedBitList(tag, integralValue); + } + + // T-REC-X.680-201508 sec 22 + // T-REC-X.690-201508 sec 8.6, 11.2.2 + private void WriteNamedBitList(Asn1Tag tag, ulong integralValue) + { + Span<byte> temp = stackalloc byte[sizeof(ulong)]; + // Reset to all zeros, since we're just going to or-in bits we need. + temp.Clear(); + + int indexOfHighestSetBit = -1; + + for (int i = 0; integralValue != 0; integralValue >>= 1, i++) + { + if ((integralValue & 1) != 0) + { + temp[i / 8] |= (byte)(0x80 >> (i % 8)); + indexOfHighestSetBit = i; + } + } + + if (indexOfHighestSetBit < 0) + { + // No bits were set; this is an empty bit string. + // T-REC-X.690-201508 sec 11.2.2-note2 + WriteBitString(tag, ReadOnlySpan<byte>.Empty); + } + else + { + // At least one bit was set. + // Determine the shortest length necessary to represent the bit string. + + // Since "bit 0" gets written down 0 => 1. + // Since "bit 8" is in the second byte 8 => 2. + // That makes the formula ((bit / 8) + 1) instead of ((bit + 7) / 8). + int byteLen = (indexOfHighestSetBit / 8) + 1; + int unusedBitCount = 7 - (indexOfHighestSetBit % 8); + + WriteBitString( + tag, + temp.Slice(0, byteLen), + unusedBitCount); + } + } + + public void WriteOctetString(ReadOnlySpan<byte> octetString) + { + WriteOctetString(Asn1Tag.PrimitiveOctetString, octetString); + } + + public void WriteOctetString(Asn1Tag tag, ReadOnlySpan<byte> octetString) + { + CheckUniversalTag(tag, UniversalTagNumber.OctetString); + + // Primitive or constructed, doesn't matter. + WriteOctetStringCore(tag, octetString); + } + + // T-REC-X.690-201508 sec 8.7 + private void WriteOctetStringCore(Asn1Tag tag, ReadOnlySpan<byte> octetString) + { + if (RuleSet == AsnEncodingRules.CER) + { + // If it's bigger than a primitive segment, use the constructed encoding + // T-REC-X.690-201508 sec 9.2 + if (octetString.Length > AsnReader.MaxCERSegmentSize) + { + WriteConstructedCerOctetString(tag, octetString); + return; + } + } + + // Clear the constructed flag, if present. + WriteTag(tag.AsPrimitive()); + WriteLength(octetString.Length); + octetString.CopyTo(_buffer.AsSpan().Slice(_offset)); + _offset += octetString.Length; + } + + // T-REC-X.690-201508 sec 9.2, 8.7 + private void WriteConstructedCerOctetString(Asn1Tag tag, ReadOnlySpan<byte> payload) + { + const int MaxCERSegmentSize = AsnReader.MaxCERSegmentSize; + Debug.Assert(payload.Length > MaxCERSegmentSize); + + WriteTag(tag.AsConstructed()); + WriteLength(-1); + + int fullSegments = Math.DivRem(payload.Length, MaxCERSegmentSize, out int lastSegmentSize); + + // The tag size is 1 byte. + // The length will always be encoded as 82 03 E8 (3 bytes) + // And 1000 content octets (by T-REC-X.690-201508 sec 9.2) + const int FullSegmentEncodedSize = 1004; + Debug.Assert( + FullSegmentEncodedSize == 1 + 1 + MaxCERSegmentSize + GetEncodedLengthSubsequentByteCount(MaxCERSegmentSize)); + + int remainingEncodedSize; + + if (lastSegmentSize == 0) + { + remainingEncodedSize = 0; + } + else + { + // One byte of tag, and minimum one byte of length. + remainingEncodedSize = 2 + lastSegmentSize + GetEncodedLengthSubsequentByteCount(lastSegmentSize); + } + + // Reduce the number of copies by pre-calculating the size. + // +2 for End-Of-Contents + int expectedSize = fullSegments * FullSegmentEncodedSize + remainingEncodedSize + 2; + EnsureWriteCapacity(expectedSize); + + byte[] ensureNoExtraCopy = _buffer; + int savedOffset = _offset; + + ReadOnlySpan<byte> remainingData = payload; + Span<byte> dest; + Asn1Tag primitiveOctetString = Asn1Tag.PrimitiveOctetString; + + while (remainingData.Length > MaxCERSegmentSize) + { + // T-REC-X.690-201508 sec 8.7.3.2-note2 + WriteTag(primitiveOctetString); + WriteLength(MaxCERSegmentSize); + + dest = _buffer.AsSpan().Slice(_offset); + remainingData.Slice(0, MaxCERSegmentSize).CopyTo(dest); + + _offset += MaxCERSegmentSize; + remainingData = remainingData.Slice(MaxCERSegmentSize); + } + + WriteTag(primitiveOctetString); + WriteLength(remainingData.Length); + dest = _buffer.AsSpan().Slice(_offset); + remainingData.CopyTo(dest); + _offset += remainingData.Length; + + WriteEndOfContents(); + + Debug.Assert(_offset - savedOffset == expectedSize, $"expected size was {expectedSize}, actual was {_offset - savedOffset}"); + Debug.Assert(_buffer == ensureNoExtraCopy, $"_buffer was replaced during {nameof(WriteConstructedCerOctetString)}"); + } + + public void WriteNull() + { + WriteNullCore(Asn1Tag.Null); + } + + public void WriteNull(Asn1Tag tag) + { + CheckUniversalTag(tag, UniversalTagNumber.Null); + + WriteNullCore(tag.AsPrimitive()); + } + + // T-REC-X.690-201508 sec 8.8 + private void WriteNullCore(Asn1Tag tag) + { + Debug.Assert(!tag.IsConstructed); + WriteTag(tag); + WriteLength(0); + } + + public void WriteObjectIdentifier(Oid oid) + { + if (oid == null) + throw new ArgumentNullException(nameof(oid)); + + WriteObjectIdentifier(oid.Value); + } + + public void WriteObjectIdentifier(string oidValue) + { + if (oidValue == null) + throw new ArgumentNullException(nameof(oidValue)); + + WriteObjectIdentifier(oidValue.AsReadOnlySpan()); + } + + public void WriteObjectIdentifier(ReadOnlySpan<char> oidValue) + { + WriteObjectIdentifierCore(Asn1Tag.ObjectIdentifier, oidValue); + } + + public void WriteObjectIdentifier(Asn1Tag tag, Oid oid) + { + if (oid == null) + throw new ArgumentNullException(nameof(oid)); + + WriteObjectIdentifier(tag, oid.Value); + } + + public void WriteObjectIdentifier(Asn1Tag tag, string oidValue) + { + if (oidValue == null) + throw new ArgumentNullException(nameof(oidValue)); + + WriteObjectIdentifier(tag, oidValue.AsReadOnlySpan()); + } + + public void WriteObjectIdentifier(Asn1Tag tag, ReadOnlySpan<char> oidValue) + { + CheckUniversalTag(tag, UniversalTagNumber.ObjectIdentifier); + + WriteObjectIdentifierCore(tag.AsPrimitive(), oidValue); + } + + // T-REC-X.690-201508 sec 8.19 + private void WriteObjectIdentifierCore(Asn1Tag tag, ReadOnlySpan<char> oidValue) + { + // T-REC-X.690-201508 sec 8.19.4 + // The first character is in { 0, 1, 2 }, the second will be a '.', and a third (digit) + // will also exist. + if (oidValue.Length < 3) + throw new CryptographicException(SR.Argument_InvalidOidValue); + if (oidValue[1] != '.') + throw new CryptographicException(SR.Argument_InvalidOidValue); + + // The worst case is "1.1.1.1.1", which takes 4 bytes (5 components, with the first two condensed) + // Longer numbers get smaller: "2.1.127" is only 2 bytes. (81d (0x51) and 127 (0x7F)) + // So length / 2 should prevent any reallocations. + byte[] tmp = ArrayPool<byte>.Shared.Rent(oidValue.Length / 2); + int tmpOffset = 0; + + try + { + int firstComponent; + + switch (oidValue[0]) + { + case '0': + firstComponent = 0; + break; + case '1': + firstComponent = 1; + break; + case '2': + firstComponent = 2; + break; + default: + throw new CryptographicException(SR.Argument_InvalidOidValue); + } + + // The first two components are special: + // ITU X.690 8.19.4: + // The numerical value of the first subidentifier is derived from the values of the first two + // object identifier components in the object identifier value being encoded, using the formula: + // (X*40) + Y + // where X is the value of the first object identifier component and Y is the value of the + // second object identifier component. + // NOTE - This packing of the first two object identifier components recognizes that only + // three values are allocated from the root node, and at most 39 subsequent values from + // nodes reached by X = 0 and X = 1. + + // skip firstComponent and the trailing . + ReadOnlySpan<char> remaining = oidValue.Slice(2); + + BigInteger subIdentifier = ParseSubIdentifier(ref remaining); + subIdentifier += 40 * firstComponent; + + int localLen = EncodeSubIdentifier(tmp.AsSpan().Slice(tmpOffset), ref subIdentifier); + tmpOffset += localLen; + + while (!remaining.IsEmpty) + { + subIdentifier = ParseSubIdentifier(ref remaining); + localLen = EncodeSubIdentifier(tmp.AsSpan().Slice(tmpOffset), ref subIdentifier); + tmpOffset += localLen; + } + + Debug.Assert(!tag.IsConstructed); + WriteTag(tag); + WriteLength(tmpOffset); + Buffer.BlockCopy(tmp, 0, _buffer, _offset, tmpOffset); + _offset += tmpOffset; + } + finally + { + Array.Clear(tmp, 0, tmpOffset); + ArrayPool<byte>.Shared.Return(tmp); + } + } + + private static BigInteger ParseSubIdentifier(ref ReadOnlySpan<char> oidValue) + { + int endIndex = oidValue.IndexOf('.'); + + if (endIndex == -1) + { + endIndex = oidValue.Length; + } + else if (endIndex == oidValue.Length - 1) + { + throw new CryptographicException(SR.Argument_InvalidOidValue); + } + + // The following code is equivalent to + // BigInteger.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out value) + // TODO: Split this for netstandard vs netcoreapp for span-perf?. + BigInteger value = BigInteger.Zero; + + for (int position = 0; position < endIndex; position++) + { + if (position > 0 && value == 0) + { + // T-REC X.680-201508 sec 12.26 + throw new CryptographicException(SR.Argument_InvalidOidValue); + } + + value *= 10; + value += AtoI(oidValue[position]); + } + + oidValue = oidValue.Slice(Math.Min(oidValue.Length, endIndex + 1)); + return value; + } + + private static int AtoI(char c) + { + if (c >= '0' && c <= '9') + { + return c - '0'; + } + + throw new CryptographicException(SR.Argument_InvalidOidValue); + } + + // ITU-T-X.690-201508 sec 8.19.5 + private static int EncodeSubIdentifier(Span<byte> dest, ref BigInteger subIdentifier) + { + Debug.Assert(dest.Length > 0); + + if (subIdentifier.IsZero) + { + dest[0] = 0; + return 1; + } + + BigInteger unencoded = subIdentifier; + int idx = 0; + + do + { + BigInteger cur = unencoded & 0x7F; + byte curByte = (byte)cur; + + if (subIdentifier != unencoded) + { + curByte |= 0x80; + } + + unencoded >>= 7; + dest[idx] = curByte; + idx++; + } + while (unencoded != BigInteger.Zero); + + Reverse(dest.Slice(0, idx)); + return idx; + } + + public void WriteEnumeratedValue(object enumValue) + { + if (enumValue == null) + throw new ArgumentNullException(nameof(enumValue)); + + WriteEnumeratedValue(Asn1Tag.Enumerated, enumValue); + } + + public void WriteEnumeratedValue<TEnum>(TEnum value) where TEnum : struct + { + WriteEnumeratedValue(Asn1Tag.Enumerated, value); + } + + public void WriteEnumeratedValue(Asn1Tag tag, object enumValue) + { + if (enumValue == null) + throw new ArgumentNullException(nameof(enumValue)); + + WriteEnumeratedValue(tag.AsPrimitive(), enumValue.GetType(), enumValue); + } + + public void WriteEnumeratedValue<TEnum>(Asn1Tag tag, TEnum value) where TEnum : struct + { + WriteEnumeratedValue(tag.AsPrimitive(), typeof(TEnum), value); + } + + // T-REC-X.690-201508 sec 8.4 + private void WriteEnumeratedValue(Asn1Tag tag, Type tEnum, object enumValue) + { + CheckUniversalTag(tag, UniversalTagNumber.Enumerated); + + Type backingType = tEnum.GetEnumUnderlyingType(); + + if (tEnum.IsDefined(typeof(FlagsAttribute), false)) + { + throw new ArgumentException( + SR.Cryptography_Asn_EnumeratedValueRequiresNonFlagsEnum, + nameof(tEnum)); + } + + if (backingType == typeof(ulong)) + { + ulong numericValue = Convert.ToUInt64(enumValue); + // T-REC-X.690-201508 sec 8.4 + WriteNonNegativeIntegerCore(tag, numericValue); + } + else + { + // All other types fit in a (signed) long. + long numericValue = Convert.ToInt64(enumValue); + // T-REC-X.690-201508 sec 8.4 + WriteIntegerCore(tag, numericValue); + } + } + + public void PushSequence() + { + PushSequenceCore(Asn1Tag.Sequence); + } + + public void PushSequence(Asn1Tag tag) + { + CheckUniversalTag(tag, UniversalTagNumber.Sequence); + + // Assert the constructed flag, in case it wasn't. + PushSequenceCore(tag.AsConstructed()); + } + + // T-REC-X.690-201508 sec 8.9, 8.10 + private void PushSequenceCore(Asn1Tag tag) + { + PushTag(tag.AsConstructed()); + } + + public void PopSequence() + { + PopSequence(Asn1Tag.Sequence); + } + + public void PopSequence(Asn1Tag tag) + { + // PopSequence shouldn't be used to pop a SetOf. + CheckUniversalTag(tag, UniversalTagNumber.Sequence); + + // Assert the constructed flag, in case it wasn't. + PopSequenceCore(tag.AsConstructed()); + } + + // T-REC-X.690-201508 sec 8.9, 8.10 + private void PopSequenceCore(Asn1Tag tag) + { + PopTag(tag); + } + + public void PushSetOf() + { + PushSetOf(Asn1Tag.SetOf); + } + + public void PushSetOf(Asn1Tag tag) + { + CheckUniversalTag(tag, UniversalTagNumber.SetOf); + + // Assert the constructed flag, in case it wasn't. + PushSetOfCore(tag.AsConstructed()); + } + + // T-REC-X.690-201508 sec 8.12 + // The writer claims SetOf, and not Set, so as to avoid the field + // ordering clause of T-REC-X.690-201508 sec 9.3 + private void PushSetOfCore(Asn1Tag tag) + { + PushTag(tag); + } + + public void PopSetOf() + { + PopSetOfCore(Asn1Tag.SetOf); + } + + public void PopSetOf(Asn1Tag tag) + { + CheckUniversalTag(tag, UniversalTagNumber.SetOf); + + // Assert the constructed flag, in case it wasn't. + PopSetOfCore(tag.AsConstructed()); + } + + // T-REC-X.690-201508 sec 8.12 + private void PopSetOfCore(Asn1Tag tag) + { + // T-REC-X.690-201508 sec 11.6 + bool sortContents = RuleSet == AsnEncodingRules.CER || RuleSet == AsnEncodingRules.DER; + + PopTag(tag, sortContents); + } + + public void WriteUtcTime(DateTimeOffset value) + { + WriteUtcTimeCore(Asn1Tag.UtcTime, value); + } + + public void WriteUtcTime(Asn1Tag tag, DateTimeOffset value) + { + CheckUniversalTag(tag, UniversalTagNumber.UtcTime); + + // Clear the constructed flag, if present. + WriteUtcTimeCore(tag.AsPrimitive(), value); + } + + // T-REC-X.680-201508 sec 47 + // T-REC-X.690-201508 sec 11.8 + private void WriteUtcTimeCore(Asn1Tag tag, DateTimeOffset value) + { + // Because UtcTime is IMPLICIT VisibleString it technically can have + // a constructed form. + // DER says character strings must be primitive. + // CER says character strings <= 1000 encoded bytes must be primitive. + // So we'll just make BER be primitive, too. + Debug.Assert(!tag.IsConstructed); + WriteTag(tag); + + // BER allows for omitting the seconds, but that's not an option we need to expose. + // BER allows for non-UTC values, but that's also not an option we need to expose. + // So the format is always yyMMddHHmmssZ (13) + const int UtcTimeValueLength = 13; + WriteLength(UtcTimeValueLength); + + DateTimeOffset normalized = value.ToUniversalTime(); + + int year = normalized.Year; + int month = normalized.Month; + int day = normalized.Day; + int hour = normalized.Hour; + int minute = normalized.Minute; + int second = normalized.Second; + + Span<byte> baseSpan = _buffer.AsSpan().Slice(_offset); + StandardFormat format = new StandardFormat('D', 2); + + if (!Utf8Formatter.TryFormat(year % 100, baseSpan.Slice(0, 2), out _, format) || + !Utf8Formatter.TryFormat(month, baseSpan.Slice(2, 2), out _, format) || + !Utf8Formatter.TryFormat(day, baseSpan.Slice(4, 2), out _, format) || + !Utf8Formatter.TryFormat(hour, baseSpan.Slice(6, 2), out _, format) || + !Utf8Formatter.TryFormat(minute, baseSpan.Slice(8, 2), out _, format) || + !Utf8Formatter.TryFormat(second, baseSpan.Slice(10, 2), out _, format)) + { + Debug.Fail($"Utf8Formatter.TryFormat failed to build components of {normalized:O}"); + throw new CryptographicException(); + } + + _buffer[_offset + 12] = (byte)'Z'; + + _offset += UtcTimeValueLength; + } + + public void WriteGeneralizedTime(DateTimeOffset value, bool omitFractionalSeconds = false) + { + WriteGeneralizedTimeCore(Asn1Tag.GeneralizedTime, value, omitFractionalSeconds); + } + + public void WriteGeneralizedTime(Asn1Tag tag, DateTimeOffset value, bool omitFractionalSeconds = false) + { + CheckUniversalTag(tag, UniversalTagNumber.GeneralizedTime); + + // Clear the constructed flag, if present. + WriteGeneralizedTimeCore(tag.AsPrimitive(), value, omitFractionalSeconds); + } + + // T-REC-X.680-201508 sec 46 + // T-REC-X.690-201508 sec 11.7 + private void WriteGeneralizedTimeCore(Asn1Tag tag, DateTimeOffset value, bool omitFractionalSeconds) + { + // GeneralizedTime under BER allows many different options: + // * (HHmmss), (HHmm), (HH) + // * "(value).frac", "(value),frac" + // * frac == 0 may be omitted or emitted + // non-UTC offset in various formats + // + // We're not allowing any of them. + // Just encode as the CER/DER common restrictions. + // + // This results in the following formats: + // yyyyMMddHHmmssZ + // yyyyMMddHHmmss.f?Z + // + // where "f?" is anything from "f" to "fffffff" (tenth of a second down to 100ns/1-tick) + // with no trailing zeros. + DateTimeOffset normalized = value.ToUniversalTime(); + + if (normalized.Year > 9999) + { + // This is unreachable since DateTimeOffset guards against this internally. + throw new ArgumentOutOfRangeException(nameof(value)); + } + + // We're only loading in sub-second ticks. + // Ticks are defined as 1e-7 seconds, so their printed form + // is at the longest "0.1234567", or 9 bytes. + Span<byte> fraction = stackalloc byte[0]; + + if (!omitFractionalSeconds) + { + long floatingTicks = normalized.Ticks % TimeSpan.TicksPerSecond; + + if (floatingTicks != 0) + { + // We're only loading in sub-second ticks. + // Ticks are defined as 1e-7 seconds, so their printed form + // is at the longest "0.1234567", or 9 bytes. + fraction = stackalloc byte[9]; + + decimal decimalTicks = floatingTicks; + decimalTicks /= TimeSpan.TicksPerSecond; + + if (!Utf8Formatter.TryFormat(decimalTicks, fraction, out int bytesWritten, new StandardFormat('G'))) + { + Debug.Fail($"Utf8Formatter.TryFormat could not format {floatingTicks} / TicksPerSecond"); + throw new CryptographicException(); + } + + Debug.Assert(bytesWritten > 2, $"{bytesWritten} should be > 2"); + Debug.Assert(fraction[0] == (byte)'0'); + Debug.Assert(fraction[1] == (byte)'.'); + + fraction = fraction.Slice(1, bytesWritten - 1); + } + } + + // yyyy, MM, dd, hh, mm, ss + const int IntegerPortionLength = 4 + 2 + 2 + 2 + 2 + 2; + // Z, and the optional fraction. + int totalLength = IntegerPortionLength + 1 + fraction.Length; + + // Because GeneralizedTime is IMPLICIT VisibleString it technically can have + // a constructed form. + // DER says character strings must be primitive. + // CER says character strings <= 1000 encoded bytes must be primitive. + // So we'll just make BER be primitive, too. + Debug.Assert(!tag.IsConstructed); + WriteTag(tag); + WriteLength(totalLength); + + int year = normalized.Year; + int month = normalized.Month; + int day = normalized.Day; + int hour = normalized.Hour; + int minute = normalized.Minute; + int second = normalized.Second; + + Span<byte> baseSpan = _buffer.AsSpan().Slice(_offset); + StandardFormat d4 = new StandardFormat('D', 4); + StandardFormat d2 = new StandardFormat('D', 2); + + if (!Utf8Formatter.TryFormat(year, baseSpan.Slice(0, 4), out _, d4) || + !Utf8Formatter.TryFormat(month, baseSpan.Slice(4, 2), out _, d2) || + !Utf8Formatter.TryFormat(day, baseSpan.Slice(6, 2), out _, d2) || + !Utf8Formatter.TryFormat(hour, baseSpan.Slice(8, 2), out _, d2) || + !Utf8Formatter.TryFormat(minute, baseSpan.Slice(10, 2), out _, d2) || + !Utf8Formatter.TryFormat(second, baseSpan.Slice(12, 2), out _, d2)) + { + Debug.Fail($"Utf8Formatter.TryFormat failed to build components of {normalized:O}"); + throw new CryptographicException(); + } + + _offset += IntegerPortionLength; + fraction.CopyTo(baseSpan.Slice(IntegerPortionLength)); + _offset += fraction.Length; + + _buffer[_offset] = (byte)'Z'; + _offset++; + } + + /// <summary> + /// Transfer the encoded representation of the data to <paramref name="dest"/>. + /// </summary> + /// <param name="dest">Write destination.</param> + /// <param name="bytesWritten">The number of bytes which were written to <paramref name="dest"/>.</param> + /// <returns><c>true</c> if the encode succeeded, <c>false</c> if <paramref name="dest"/> is too small.</returns> + /// <exception cref="InvalidOperationException"> + /// A <see cref="PushSequence()"/> or <see cref="PushSetOf()"/> has not been closed via + /// <see cref="PopSequence()"/> or <see cref="PopSetOf()"/>. + /// </exception> + public bool TryEncode(Span<byte> dest, out int bytesWritten) + { + if ((_nestingStack?.Count ?? 0) != 0) + throw new InvalidOperationException(SR.Cryptography_AsnWriter_EncodeUnbalancedStack); + + // If the stack is closed out then everything is a definite encoding (BER, DER) or a + // required indefinite encoding (CER). So we're correctly sized up, and ready to copy. + if (dest.Length < _offset) + { + bytesWritten = 0; + return false; + } + + if (_offset == 0) + { + bytesWritten = 0; + return true; + } + + bytesWritten = _offset; + _buffer.AsSpan().Slice(0, _offset).CopyTo(dest); + return true; + } + + public byte[] Encode() + { + if ((_nestingStack?.Count ?? 0) != 0) + { + throw new InvalidOperationException(SR.Cryptography_AsnWriter_EncodeUnbalancedStack); + } + + if (_offset == 0) + { + return Array.Empty<byte>(); + } + + // If the stack is closed out then everything is a definite encoding (BER, DER) or a + // required indefinite encoding (CER). So we're correctly sized up, and ready to copy. + return _buffer.AsSpan().Slice(0, _offset).ToArray(); + } + + private void PushTag(Asn1Tag tag) + { + if (_nestingStack == null) + { + _nestingStack = new Stack<(Asn1Tag,int)>(); + } + + Debug.Assert(tag.IsConstructed); + WriteTag(tag); + _nestingStack.Push((tag, _offset)); + // Indicate that the length is indefinite. + // We'll come back and clean this up (as appropriate) in PopTag. + WriteLength(-1); + } + + private void PopTag(Asn1Tag tag, bool sortContents=false) + { + if (_nestingStack == null || _nestingStack.Count == 0) + { + throw new ArgumentException(SR.Cryptography_AsnWriter_PopWrongTag, nameof(tag)); + } + + (Asn1Tag stackTag, int lenOffset) = _nestingStack.Peek(); + + Debug.Assert(tag.IsConstructed); + if (stackTag != tag) + { + throw new ArgumentException(SR.Cryptography_AsnWriter_PopWrongTag, nameof(tag)); + } + + _nestingStack.Pop(); + + if (sortContents) + { + SortContents(_buffer, lenOffset + 1, _offset); + } + + // BER could use the indefinite encoding that CER does. + // But since the definite encoding form is easier to read (doesn't require a contextual + // parser to find the end-of-contents marker) some ASN.1 readers (including the previous + // incarnation of AsnReader) may choose not to support it. + // + // So, BER will use the DER rules here, in the interest of broader compatibility. + + // T-REC-X.690-201508 sec 9.1 (constructed CER => indefinite length) + // T-REC-X.690-201508 sec 8.1.3.6 + if (RuleSet == AsnEncodingRules.CER) + { + WriteEndOfContents(); + return; + } + + int containedLength = _offset - 1 - lenOffset; + Debug.Assert(containedLength >= 0); + + int shiftSize = GetEncodedLengthSubsequentByteCount(containedLength); + + // Best case, length fits in the compact byte + if (shiftSize == 0) + { + _buffer[lenOffset] = (byte)containedLength; + return; + } + + // We're currently at the end, so ensure we have room for N more bytes. + EnsureWriteCapacity(shiftSize); + + // Buffer.BlockCopy correctly does forward-overlapped, so use it. + int start = lenOffset + 1; + Buffer.BlockCopy(_buffer, start, _buffer, start + shiftSize, containedLength); + + int tmp = _offset; + _offset = lenOffset; + WriteLength(containedLength); + Debug.Assert(_offset - lenOffset - 1 == shiftSize); + _offset = tmp + shiftSize; + } + + public void WriteCharacterString(UniversalTagNumber encodingType, string str) + { + if (str == null) + throw new ArgumentNullException(nameof(str)); + + WriteCharacterString(encodingType, str.AsReadOnlySpan()); + } + + public void WriteCharacterString(UniversalTagNumber encodingType, ReadOnlySpan<char> str) + { + Text.Encoding encoding = AsnCharacterStringEncodings.GetEncoding(encodingType); + + WriteCharacterStringCore(new Asn1Tag(encodingType), encoding, str); + } + + public void WriteCharacterString(Asn1Tag tag, UniversalTagNumber encodingType, string str) + { + if (str == null) + throw new ArgumentNullException(nameof(str)); + + WriteCharacterString(tag, encodingType, str.AsReadOnlySpan()); + } + + public void WriteCharacterString(Asn1Tag tag, UniversalTagNumber encodingType, ReadOnlySpan<char> str) + { + CheckUniversalTag(tag, encodingType); + + Text.Encoding encoding = AsnCharacterStringEncodings.GetEncoding(encodingType); + WriteCharacterStringCore(tag, encoding, str); + } + + // T-REC-X.690-201508 sec 8.23 + private void WriteCharacterStringCore(Asn1Tag tag, Text.Encoding encoding, ReadOnlySpan<char> str) + { + int size = -1; + + // T-REC-X.690-201508 sec 9.2 + if (RuleSet == AsnEncodingRules.CER) + { + // TODO: Split this for netstandard vs netcoreapp for span?. + unsafe + { + fixed (char* strPtr = &str.DangerousGetPinnableReference()) + { + size = encoding.GetByteCount(strPtr, str.Length); + + // If it exceeds the primitive segment size, use the constructed encoding. + if (size > AsnReader.MaxCERSegmentSize) + { + WriteConstructedCerCharacterString(tag, encoding, str, size); + return; + } + } + } + } + + // TODO: Split this for netstandard vs netcoreapp for span?. + unsafe + { + fixed (char* strPtr = &str.DangerousGetPinnableReference()) + { + if (size < 0) + { + size = encoding.GetByteCount(strPtr, str.Length); + } + + // Clear the constructed tag, if present. + WriteTag(tag.AsPrimitive()); + WriteLength(size); + Span<byte> dest = _buffer.AsSpan().Slice(_offset, size); + + fixed (byte* destPtr = &dest.DangerousGetPinnableReference()) + { + int written = encoding.GetBytes(strPtr, str.Length, destPtr, dest.Length); + + if (written != size) + { + Debug.Fail($"Encoding produced different answer for GetByteCount ({size}) and GetBytes ({written})"); + throw new InvalidOperationException(); + } + } + + _offset += size; + } + } + } + + private void WriteConstructedCerCharacterString(Asn1Tag tag, Text.Encoding encoding, ReadOnlySpan<char> str, int size) + { + Debug.Assert(size > AsnReader.MaxCERSegmentSize); + + byte[] tmp; + + // TODO: Split this for netstandard vs netcoreapp for span?. + unsafe + { + fixed (char* strPtr = &str.DangerousGetPinnableReference()) + { + tmp = ArrayPool<byte>.Shared.Rent(size); + + fixed (byte* destPtr = tmp) + { + int written = encoding.GetBytes(strPtr, str.Length, destPtr, tmp.Length); + + if (written != size) + { + Debug.Fail( + $"Encoding produced different answer for GetByteCount ({size}) and GetBytes ({written})"); + throw new InvalidOperationException(); + } + } + } + } + + WriteConstructedCerOctetString(tag, tmp.AsSpan().Slice(0, size)); + Array.Clear(tmp, 0, size); + ArrayPool<byte>.Shared.Return(tmp); + } + + private static void SortContents(byte[] buffer, int start, int end) + { + Debug.Assert(buffer != null); + Debug.Assert(end >= start); + + int len = end - start; + + if (len == 0) + { + return; + } + + // Since BER can read everything and the reader does not mutate data + // just use a BER reader for identifying the positions of the values + // within this memory segment. + // + // Since it's not mutating, any restrictions imposed by CER or DER will + // still be maintained. + var reader = new AsnReader(new ReadOnlyMemory<byte>(buffer, start, len), AsnEncodingRules.BER); + + List<(int, int)> positions = new List<(int, int)>(); + + int pos = start; + + while (reader.HasData) + { + ReadOnlyMemory<byte> encoded = reader.GetEncodedValue(); + positions.Add((pos, encoded.Length)); + pos += encoded.Length; + } + + Debug.Assert(pos == end); + + var comparer = new ArrayIndexSetOfValueComparer(buffer); + positions.Sort(comparer); + + byte[] tmp = ArrayPool<byte>.Shared.Rent(len); + + pos = 0; + + foreach ((int offset, int length) in positions) + { + Buffer.BlockCopy(buffer, offset, tmp, pos, length); + pos += length; + } + + Debug.Assert(pos == len); + + Buffer.BlockCopy(tmp, 0, buffer, start, len); + Array.Clear(tmp, 0, len); + ArrayPool<byte>.Shared.Return(tmp); + } + + private static void Reverse(Span<byte> span) + { + int i = 0; + int j = span.Length - 1; + + while (i < j) + { + byte tmp = span[i]; + span[i] = span[j]; + span[j] = tmp; + + i++; + j--; + } + } + + private static void CheckUniversalTag(Asn1Tag tag, UniversalTagNumber universalTagNumber) + { + if (tag.TagClass == TagClass.Universal && tag.TagValue != (int)universalTagNumber) + { + throw new ArgumentException( + SR.Cryptography_Asn_UniversalValueIsFixed, + nameof(tag)); + } + } + + private class ArrayIndexSetOfValueComparer : IComparer<(int, int)> + { + private readonly byte[] _data; + + public ArrayIndexSetOfValueComparer(byte[] data) + { + _data = data; + } + + public int Compare((int, int) x, (int, int) y) + { + (int xOffset, int xLength) = x; + (int yOffset, int yLength) = y; + + int value = + SetOfValueComparer.Instance.Compare( + new ReadOnlyMemory<byte>(_data, xOffset, xLength), + new ReadOnlyMemory<byte>(_data, yOffset, yLength)); + + if (value == 0) + { + // Whichever had the lowest index wins (once sorted, stay sorted) + return xOffset - yOffset; + } + + return value; + } + } + } +} diff --git a/src/Common/tests/System/Security/Cryptography/ByteUtils.cs b/src/Common/tests/System/Security/Cryptography/ByteUtils.cs index 38c8c5f918..f39df0409f 100644 --- a/src/Common/tests/System/Security/Cryptography/ByteUtils.cs +++ b/src/Common/tests/System/Security/Cryptography/ByteUtils.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Globalization; using System.Text; @@ -36,11 +37,26 @@ namespace Test.Cryptography internal static string ByteArrayToHex(this byte[] bytes) { + return ByteArrayToHex(bytes.AsReadOnlySpan()); + } + + internal static string ByteArrayToHex(this Span<byte> bytes) + { + return ByteArrayToHex((ReadOnlySpan<byte>)bytes); + } + + internal static string ByteArrayToHex(this ReadOnlyMemory<byte> bytes) + { + return ByteArrayToHex(bytes.Span); + } + + internal static string ByteArrayToHex(this ReadOnlySpan<byte> bytes) + { StringBuilder builder = new StringBuilder(bytes.Length * 2); - foreach (byte b in bytes) + for (int i = 0; i < bytes.Length; i++) { - builder.Append(b.ToString("X2")); + builder.Append(bytes[i].ToString("X2")); } return builder.ToString(); diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/Asn1ReaderTests.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/Asn1ReaderTests.cs new file mode 100644 index 0000000000..a609c6661e --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/Asn1ReaderTests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public abstract partial class Asn1ReaderTests + { + public enum PublicTagClass : byte + { + Universal = TagClass.Universal, + Application = TagClass.Application, + ContextSpecific = TagClass.ContextSpecific, + Private = TagClass.Private, + } + + public enum PublicEncodingRules + { + BER = AsnEncodingRules.BER, + CER = AsnEncodingRules.CER, + DER = AsnEncodingRules.DER, + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ComprehensiveReadTests.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ComprehensiveReadTests.cs new file mode 100644 index 0000000000..768a56e948 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ComprehensiveReadTests.cs @@ -0,0 +1,265 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +using X509KeyUsageCSharpStyle=System.Security.Cryptography.Tests.Asn1.ReadNamedBitList.X509KeyUsageCSharpStyle; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public static class ComprehensiveReadTests + { + [Fact] + public static void ReadMicrosoftComCert() + { + byte[] bytes = MicrosoftDotComSslCertBytes; + AsnReader fileReader = new AsnReader(bytes, AsnEncodingRules.DER); + + AsnReader certReader = fileReader.ReadSequence(); + Assert.False(fileReader.HasData, "fileReader.HasData"); + + AsnReader tbsCertReader = certReader.ReadSequence(); + AsnReader sigAlgReader = certReader.ReadSequence(); + + Assert.True( + certReader.TryGetPrimitiveBitStringValue( + out int unusedBitCount, + out ReadOnlyMemory<byte> signature), + "certReader.TryGetBitStringBytes"); + + Assert.Equal(0, unusedBitCount); + AssertRefSame(signature, ref bytes[1176], "Signature is a ref to bytes[1176]"); + + Assert.False(certReader.HasData, "certReader.HasData"); + + AsnReader versionExplicitWrapper = tbsCertReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0)); + Assert.True(versionExplicitWrapper.TryReadInt32(out int certVersion)); + Assert.Equal(2, certVersion); + Assert.False(versionExplicitWrapper.HasData, "versionExplicitWrapper.HasData"); + + ReadOnlyMemory<byte> serialBytes = tbsCertReader.GetIntegerBytes(); + AssertRefSame(serialBytes, ref bytes[15], "Serial number starts at bytes[15]"); + + AsnReader tbsSigAlgReader = tbsCertReader.ReadSequence(); + Assert.Equal("1.2.840.113549.1.1.11", tbsSigAlgReader.ReadObjectIdentifierAsString()); + Assert.True(tbsSigAlgReader.HasData, "tbsSigAlgReader.HasData before ReadNull"); + tbsSigAlgReader.ReadNull(); + Assert.False(tbsSigAlgReader.HasData, "tbsSigAlgReader.HasData after ReadNull"); + + AsnReader issuerReader = tbsCertReader.ReadSequence(); + Asn1Tag printableString = new Asn1Tag(UniversalTagNumber.PrintableString); + AssertRdn(issuerReader, "2.5.4.6", 57, printableString, bytes, "issuer[C]"); + AssertRdn(issuerReader, "2.5.4.10", 70, printableString, bytes, "issuer[O]"); + AssertRdn(issuerReader, "2.5.4.11", 101, printableString, bytes, "issuer[OU]"); + AssertRdn(issuerReader, "2.5.4.3", 134, printableString, bytes, "issuer[CN]"); + Assert.False(issuerReader.HasData, "issuerReader.HasData"); + + AsnReader validityReader = tbsCertReader.ReadSequence(); + Assert.Equal(new DateTimeOffset(2014, 10, 15, 0, 0, 0, TimeSpan.Zero), validityReader.GetUtcTime()); + Assert.Equal(new DateTimeOffset(2016, 10, 15, 23, 59, 59, TimeSpan.Zero), validityReader.GetUtcTime()); + Assert.False(validityReader.HasData, "validityReader.HasData"); + + AsnReader subjectReader = tbsCertReader.ReadSequence(); + Asn1Tag utf8String = new Asn1Tag(UniversalTagNumber.UTF8String); + AssertRdn(subjectReader, "1.3.6.1.4.1.311.60.2.1.3", 220, printableString, bytes, "subject[EV Country]"); + AssertRdn(subjectReader, "1.3.6.1.4.1.311.60.2.1.2", 241, utf8String, bytes, "subject[EV State]", "Washington"); + AssertRdn(subjectReader, "2.5.4.15", 262, printableString, bytes, "subject[Business Category]"); + AssertRdn(subjectReader, "2.5.4.5", 293, printableString, bytes, "subject[Serial Number]"); + AssertRdn(subjectReader, "2.5.4.6", 313, printableString, bytes, "subject[C]"); + AssertRdn(subjectReader, "2.5.4.17", 326, utf8String, bytes, "subject[Postal Code]", "98052"); + AssertRdn(subjectReader, "2.5.4.8", 342, utf8String, bytes, "subject[ST]", "Washington"); + AssertRdn(subjectReader, "2.5.4.7", 363, utf8String, bytes, "subject[L]", "Redmond"); + AssertRdn(subjectReader, "2.5.4.9", 381, utf8String, bytes, "subject[Street Address]", "1 Microsoft Way"); + AssertRdn(subjectReader, "2.5.4.10", 407, utf8String, bytes, "subject[O]", "Microsoft Corporation"); + AssertRdn(subjectReader, "2.5.4.11", 439, utf8String, bytes, "subject[OU]", "MSCOM"); + AssertRdn(subjectReader, "2.5.4.3", 455, utf8String, bytes, "subject[CN]", "www.microsoft.com"); + Assert.False(subjectReader.HasData, "subjectReader.HasData"); + + AsnReader subjectPublicKeyInfo = tbsCertReader.ReadSequence(); + AsnReader spkiAlgorithm = subjectPublicKeyInfo.ReadSequence(); + Assert.Equal("1.2.840.113549.1.1.1", spkiAlgorithm.ReadObjectIdentifierAsString()); + spkiAlgorithm.ReadNull(); + Assert.False(spkiAlgorithm.HasData, "spkiAlgorithm.HasData"); + + Assert.True( + subjectPublicKeyInfo.TryGetPrimitiveBitStringValue( + out unusedBitCount, + out ReadOnlyMemory<byte> encodedPublicKey), + "subjectPublicKeyInfo.TryGetBitStringBytes"); + + Assert.Equal(0, unusedBitCount); + AssertRefSame(encodedPublicKey, ref bytes[498], "Encoded public key starts at byte 498"); + + Assert.False(subjectPublicKeyInfo.HasData, "subjectPublicKeyInfo.HasData"); + + AsnReader publicKeyReader = new AsnReader(encodedPublicKey, AsnEncodingRules.DER); + AsnReader rsaPublicKeyReader = publicKeyReader.ReadSequence(); + AssertRefSame(rsaPublicKeyReader.GetIntegerBytes(), ref bytes[506], "RSA Modulus is at bytes[502]"); + Assert.True(rsaPublicKeyReader.TryReadInt32(out int rsaExponent)); + Assert.Equal(65537, rsaExponent); + Assert.False(rsaPublicKeyReader.HasData, "rsaPublicKeyReader.HasData"); + Assert.False(publicKeyReader.HasData, "publicKeyReader.HasData"); + + AsnReader extensionsContainer = tbsCertReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 3)); + AsnReader extensions = extensionsContainer.ReadSequence(); + Assert.False(extensionsContainer.HasData, "extensionsContainer.HasData"); + + AsnReader sanExtension = extensions.ReadSequence(); + Assert.Equal("2.5.29.17", sanExtension.ReadObjectIdentifierAsString()); + Assert.True(sanExtension.TryGetPrimitiveOctetStringBytes(out ReadOnlyMemory<byte> sanExtensionBytes)); + Assert.False(sanExtension.HasData, "sanExtension.HasData"); + + AsnReader sanExtensionPayload = new AsnReader(sanExtensionBytes, AsnEncodingRules.DER); + AsnReader sanExtensionValue = sanExtensionPayload.ReadSequence(); + Assert.False(sanExtensionPayload.HasData, "sanExtensionPayload.HasData"); + Asn1Tag dnsName = new Asn1Tag(TagClass.ContextSpecific, 2); + Assert.Equal("www.microsoft.com", sanExtensionValue.GetCharacterString(dnsName, UniversalTagNumber.IA5String)); + Assert.Equal("wwwqa.microsoft.com", sanExtensionValue.GetCharacterString(dnsName, UniversalTagNumber.IA5String)); + Assert.False(sanExtensionValue.HasData, "sanExtensionValue.HasData"); + + AsnReader basicConstraints = extensions.ReadSequence(); + Assert.Equal("2.5.29.19", basicConstraints.ReadObjectIdentifierAsString()); + Assert.True(basicConstraints.TryGetPrimitiveOctetStringBytes(out ReadOnlyMemory<byte> basicConstraintsBytes)); + + AsnReader basicConstraintsPayload = new AsnReader(basicConstraintsBytes, AsnEncodingRules.DER); + AsnReader basicConstraintsValue = basicConstraintsPayload.ReadSequence(); + Assert.False(basicConstraintsValue.HasData, "basicConstraintsValue.HasData"); + Assert.False(basicConstraintsPayload.HasData, "basicConstraintsPayload.HasData"); + + AsnReader keyUsageExtension = extensions.ReadSequence(); + Assert.Equal("2.5.29.15", keyUsageExtension.ReadObjectIdentifierAsString()); + Assert.True(keyUsageExtension.ReadBoolean(), "keyUsageExtension.ReadBoolean() (IsCritical)"); + Assert.True(keyUsageExtension.TryGetPrimitiveOctetStringBytes(out ReadOnlyMemory<byte> keyUsageBytes)); + + AsnReader keyUsagePayload = new AsnReader(keyUsageBytes, AsnEncodingRules.DER); + + Assert.Equal( + X509KeyUsageCSharpStyle.DigitalSignature | X509KeyUsageCSharpStyle.KeyEncipherment, + keyUsagePayload.GetNamedBitListValue<X509KeyUsageCSharpStyle>()); + + Assert.False(keyUsagePayload.HasData, "keyUsagePayload.HasData"); + + AssertExtension(extensions, "2.5.29.37", false, 863, bytes); + AssertExtension(extensions, "2.5.29.32", false, 894, bytes); + AssertExtension(extensions, "2.5.29.35", false, 998, bytes); + AssertExtension(extensions, "2.5.29.31", false, 1031, bytes); + AssertExtension(extensions, "1.3.6.1.5.5.7.1.1", false, 1081, bytes); + Assert.False(extensions.HasData, "extensions.HasData"); + + Assert.Equal("1.2.840.113549.1.1.11", sigAlgReader.ReadObjectIdentifierAsString()); + sigAlgReader.ReadNull(); + Assert.False(sigAlgReader.HasData); + } + + private static void AssertExtension(AsnReader extensions, string oid, bool critical, int index, byte[] bytes) + { + AsnReader extension = extensions.ReadSequence(); + Assert.Equal(oid, extension.ReadObjectIdentifierAsString()); + + if (critical) + { + Assert.True(extension.ReadBoolean(), $"{oid} is critical"); + } + + Assert.True(extension.TryGetPrimitiveOctetStringBytes(out ReadOnlyMemory<byte> extensionBytes)); + AssertRefSame(extensionBytes, ref bytes[index], $"{oid} extension value is at byte {index}"); + } + + private static void AssertRdn( + AsnReader reader, + string atvOid, + int offset, + Asn1Tag valueTag, + byte[] bytes, + string label, + string stringValue=null) + { + AsnReader rdn = reader.ReadSetOf(); + AsnReader attributeTypeAndValue = rdn.ReadSequence(); + Assert.Equal(atvOid, attributeTypeAndValue.ReadObjectIdentifierAsString()); + + ReadOnlyMemory<byte> value = attributeTypeAndValue.GetEncodedValue(); + ReadOnlySpan<byte> valueSpan = value.Span; + + Assert.True(Asn1Tag.TryParse(valueSpan, out Asn1Tag actualTag, out int bytesRead)); + Assert.Equal(1, bytesRead); + Assert.Equal(valueTag, actualTag); + + AssertRefSame( + ref valueSpan.DangerousGetPinnableReference(), + ref bytes[offset], + $"{label} is at bytes[{offset}]"); + + if (stringValue != null) + { + AsnReader valueReader = new AsnReader(value, AsnEncodingRules.DER); + Assert.Equal(stringValue, valueReader.GetCharacterString((UniversalTagNumber)valueTag.TagValue)); + Assert.False(valueReader.HasData, "valueReader.HasData"); + } + + Assert.False(attributeTypeAndValue.HasData, $"attributeTypeAndValue.HasData ({label})"); + Assert.False(rdn.HasData, $"rdn.HasData ({label})"); + } + + private static void AssertRefSame(ReadOnlyMemory<byte> a, ref byte b, string msg) + { + AssertRefSame(ref a.Span.DangerousGetPinnableReference(), ref b, msg); + } + + private static void AssertRefSame(ref byte a, ref byte b, string msg) + { + Assert.True(Unsafe.AreSame(ref a, ref b), msg); + } + + internal static readonly byte[] MicrosoftDotComSslCertBytes = ( + "308205943082047CA00302010202103DF70C5D9903F8D8868B9B8CCF20DF6930" + + "0D06092A864886F70D01010B05003077310B3009060355040613025553311D30" + + "1B060355040A131453796D616E74656320436F72706F726174696F6E311F301D" + + "060355040B131653796D616E746563205472757374204E6574776F726B312830" + + "260603550403131F53796D616E74656320436C61737320332045562053534C20" + + "4341202D204733301E170D3134313031353030303030305A170D313631303135" + + "3233353935395A3082010F31133011060B2B0601040182373C02010313025553" + + "311B3019060B2B0601040182373C0201020C0A57617368696E67746F6E311D30" + + "1B060355040F131450726976617465204F7267616E697A6174696F6E31123010" + + "06035504051309363030343133343835310B3009060355040613025553310E30" + + "0C06035504110C0539383035323113301106035504080C0A57617368696E6774" + + "6F6E3110300E06035504070C075265646D6F6E643118301606035504090C0F31" + + "204D6963726F736F667420576179311E301C060355040A0C154D6963726F736F" + + "667420436F72706F726174696F6E310E300C060355040B0C054D53434F4D311A" + + "301806035504030C117777772E6D6963726F736F66742E636F6D30820122300D" + + "06092A864886F70D01010105000382010F003082010A0282010100A46861FA9D" + + "5DB763633BF5A64EF6E7C2C2367F48D2D46643A22DFCFCCB24E58A14D0F06BDC" + + "956437F2A56BA4BEF70BA361BF12964A0D665AFD84B0F7494C8FA4ABC5FCA2E0" + + "17C06178AEF2CDAD1B5F18E997A14B965C074E8F564970607276B00583932240" + + "FE6E2DD013026F9AE13D7C91CC07C4E1E8E87737DC06EF2B575B89D62EFE4685" + + "9F8255A123692A706C68122D4DAFE11CB205A7B3DE06E553F7B95F978EF8601A" + + "8DF819BF32040BDF92A0DE0DF269B4514282E17AC69934E8440A48AB9D1F5DF8" + + "9A502CEF6DFDBE790045BD45E0C94E5CA8ADD76A013E9C978440FC8A9E2A9A49" + + "40B2460819C3E302AA9C9F355AD754C86D3ED77DDAA3DA13810B4D0203010001" + + "A38201803082017C30310603551D11042A302882117777772E6D6963726F736F" + + "66742E636F6D821377777771612E6D6963726F736F66742E636F6D3009060355" + + "1D1304023000300E0603551D0F0101FF0404030205A0301D0603551D25041630" + + "1406082B0601050507030106082B0601050507030230660603551D20045F305D" + + "305B060B6086480186F84501071706304C302306082B06010505070201161768" + + "747470733A2F2F642E73796D63622E636F6D2F637073302506082B0601050507" + + "020230191A1768747470733A2F2F642E73796D63622E636F6D2F727061301F06" + + "03551D230418301680140159ABE7DD3A0B59A66463D6CF200757D591E76A302B" + + "0603551D1F042430223020A01EA01C861A687474703A2F2F73722E73796D6362" + + "2E636F6D2F73722E63726C305706082B06010505070101044B3049301F06082B" + + "060105050730018613687474703A2F2F73722E73796D63642E636F6D30260608" + + "2B06010505073002861A687474703A2F2F73722E73796D63622E636F6D2F7372" + + "2E637274300D06092A864886F70D01010B0500038201010015F8505B627ED7F9" + + "F96707097E93A51E7A7E05A3D420A5C258EC7A1CFE1843EC20ACF728AAFA7A1A" + + "1BC222A7CDBF4AF90AA26DEEB3909C0B3FB5C78070DAE3D645BFCF840A4A3FDD" + + "988C7B3308BFE4EB3FD66C45641E96CA3352DBE2AEB4488A64A9C5FB96932BA7" + + "0059CE92BD278B41299FD213471BD8165F924285AE3ECD666C703885DCA65D24" + + "DA66D3AFAE39968521995A4C398C7DF38DFA82A20372F13D4A56ADB21B582254" + + "9918015647B5F8AC131CC5EB24534D172BC60218A88B65BCF71C7F388CE3E0EF" + + "697B4203720483BB5794455B597D80D48CD3A1D73CBBC609C058767D1FF060A6" + + "09D7E3D4317079AF0CD0A8A49251AB129157F9894A036487").HexToByteArray(); + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ParseTag.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ParseTag.cs new file mode 100644 index 0000000000..a9211778f1 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ParseTag.cs @@ -0,0 +1,160 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ParseTag : Asn1ReaderTests + { + [Theory] + [InlineData(PublicTagClass.Universal, false, 0, "00")] + [InlineData(PublicTagClass.Universal, false, 1, "01")] + [InlineData(PublicTagClass.Application, true, 1, "61")] + [InlineData(PublicTagClass.ContextSpecific, false, 1, "81")] + [InlineData(PublicTagClass.ContextSpecific, true, 1, "A1")] + [InlineData(PublicTagClass.Private, false, 1, "C1")] + [InlineData(PublicTagClass.Universal, false, 30, "1E")] + [InlineData(PublicTagClass.Application, false, 30, "5E")] + [InlineData(PublicTagClass.ContextSpecific, false, 30, "9E")] + [InlineData(PublicTagClass.Private, false, 30, "DE")] + [InlineData(PublicTagClass.Universal, false, 31, "1F1F")] + [InlineData(PublicTagClass.Application, false, 31, "5F1F")] + [InlineData(PublicTagClass.ContextSpecific, false, 31, "9F1F")] + [InlineData(PublicTagClass.Private, false, 31, "DF1F")] + [InlineData(PublicTagClass.Private, false, 127, "DF7F")] + [InlineData(PublicTagClass.Private, false, 128, "DF8100")] + [InlineData(PublicTagClass.Private, false, 253, "DF817D")] + [InlineData(PublicTagClass.Private, false, 255, "DF817F")] + [InlineData(PublicTagClass.Private, false, 256, "DF8200")] + [InlineData(PublicTagClass.Private, false, 1 << 9, "DF8400")] + [InlineData(PublicTagClass.Private, false, 1 << 10, "DF8800")] + [InlineData(PublicTagClass.Private, false, 0b0011_1101_1110_0111, "DFFB67")] + [InlineData(PublicTagClass.Private, false, 1 << 14, "DF818000")] + [InlineData(PublicTagClass.Private, false, 1 << 18, "DF908000")] + [InlineData(PublicTagClass.Private, false, 1 << 18 | 1 << 9, "DF908400")] + [InlineData(PublicTagClass.Private, false, 1 << 20, "DFC08000")] + [InlineData(PublicTagClass.Private, false, 0b0001_1110_1010_0111_0000_0001, "DFFACE01")] + [InlineData(PublicTagClass.Private, false, 1 << 21, "DF81808000")] + [InlineData(PublicTagClass.Private, false, 1 << 27, "DFC0808000")] + [InlineData(PublicTagClass.Private, false, 1 << 28, "DF8180808000")] + [InlineData(PublicTagClass.Private, true, int.MaxValue, "FF87FFFFFF7F")] + [InlineData(PublicTagClass.Universal, false, 119, "1F77")] + public static void ParseValidTag( + PublicTagClass tagClass, + bool isConstructed, + int tagValue, + string inputHex) + { + byte[] inputBytes = inputHex.HexToByteArray(); + + bool parsed = Asn1Tag.TryParse(inputBytes, out Asn1Tag tag, out int bytesRead); + + Assert.True(parsed, "Asn1Tag.TryParse"); + Assert.Equal(inputBytes.Length, bytesRead); + Assert.Equal((TagClass)tagClass, tag.TagClass); + Assert.Equal(tagValue, tag.TagValue); + + if (isConstructed) + { + Assert.True(tag.IsConstructed, "tag.IsConstructed"); + } + else + { + Assert.False(tag.IsConstructed, "tag.IsConstructed"); + } + + byte[] secondBytes = new byte[inputBytes.Length]; + int written; + Assert.False(tag.TryWrite(secondBytes.AsSpan().Slice(0, inputBytes.Length - 1), out written)); + Assert.Equal(0, written); + Assert.True(tag.TryWrite(secondBytes, out written)); + Assert.Equal(inputBytes.Length, written); + Assert.Equal(inputHex, secondBytes.ByteArrayToHex()); + } + + [Theory] + [InlineData("Empty", "")] + [InlineData("MultiByte-NoFollow", "1F")] + [InlineData("MultiByte-NoFollow2", "1F81")] + [InlineData("MultiByte-NoFollow3", "1F8180")] + [InlineData("MultiByte-TooLow", "1F01")] + [InlineData("MultiByte-TooLowMax", "1F1E")] + [InlineData("MultiByte-Leading0", "1F807F")] + [InlineData("MultiByte-ValueTooBig", "FF8880808000")] + [InlineData("MultiByte-ValueSubtlyTooBig", "DFC1C0808000")] + public static void ParseCorruptTag(string description, string inputHex) + { + byte[] inputBytes = inputHex.HexToByteArray(); + + Assert.False(Asn1Tag.TryParse(inputBytes, out Asn1Tag tag, out var bytesRead)); + + Assert.Equal(default(Asn1Tag), tag); + Assert.Equal(0, bytesRead); + } + + [Fact] + public static void TestEquals() + { + Asn1Tag integer = new Asn1Tag(TagClass.Universal, 2); + Asn1Tag integerAgain = new Asn1Tag(TagClass.Universal, 2); + Asn1Tag context2 = new Asn1Tag(TagClass.ContextSpecific, 2); + Asn1Tag constructedContext2 = new Asn1Tag(TagClass.ContextSpecific, 2, true); + Asn1Tag application2 = new Asn1Tag(TagClass.Application, 2); + + Assert.False(integer.Equals(null)); + Assert.False(integer.Equals(0x02)); + Assert.False(integer.Equals(context2)); + Assert.False(context2.Equals(constructedContext2)); + Assert.False(context2.Equals(application2)); + + Assert.Equal(integer, integerAgain); + Assert.True(integer == integerAgain); + Assert.True(integer != context2); + Assert.False(integer == context2); + Assert.False(context2 == constructedContext2); + Assert.False(context2 == application2); + + Assert.NotEqual(integer.GetHashCode(), context2.GetHashCode()); + Assert.NotEqual(context2.GetHashCode(), constructedContext2.GetHashCode()); + Assert.NotEqual(context2.GetHashCode(), application2.GetHashCode()); + Assert.Equal(integer.GetHashCode(), integerAgain.GetHashCode()); + } + + [Theory] + [InlineData(PublicTagClass.Universal, false, 0, "00")] + [InlineData(PublicTagClass.ContextSpecific, true, 1, "A1")] + [InlineData(PublicTagClass.Application, false, 31, "5F1F")] + [InlineData(PublicTagClass.Private, false, 128, "DF8100")] + [InlineData(PublicTagClass.Private, false, 0b0001_1110_1010_0111_0000_0001, "DFFACE01")] + [InlineData(PublicTagClass.Private, true, int.MaxValue, "FF87FFFFFF7F")] + public static void ParseTagWithMoreData( + PublicTagClass tagClass, + bool isConstructed, + int tagValue, + string inputHex) + { + byte[] inputBytes = inputHex.HexToByteArray(); + Array.Resize(ref inputBytes, inputBytes.Length + 3); + + bool parsed = Asn1Tag.TryParse(inputBytes, out Asn1Tag tag, out int bytesRead); + + Assert.True(parsed, "Asn1Tag.TryParse"); + Assert.Equal(inputHex.Length / 2, bytesRead); + Assert.Equal((TagClass)tagClass, tag.TagClass); + Assert.Equal(tagValue, tag.TagValue); + + if (isConstructed) + { + Assert.True(tag.IsConstructed, "tag.IsConstructed"); + } + else + { + Assert.False(tag.IsConstructed, "tag.IsConstructed"); + } + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/PeekTests.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/PeekTests.cs new file mode 100644 index 0000000000..c5814b41c8 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/PeekTests.cs @@ -0,0 +1,213 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class PeekTests : Asn1ReaderTests + { + [Fact] + public static void ReaderPeekTag_Valid() + { + // SEQUENCE(NULL) + byte[] data = { 0x30, 0x02, 0x05, 0x00 }; + AsnReader reader = new AsnReader(data, AsnEncodingRules.BER); + Asn1Tag tag = reader.PeekTag(); + + Assert.Equal((int)UniversalTagNumber.Sequence, tag.TagValue); + Assert.True(tag.IsConstructed, "tag.IsConstructed"); + Assert.Equal(TagClass.Universal, tag.TagClass); + } + + [Fact] + public static void ReaderPeekTag_Invalid() + { + // (UNIVERSAL [continue into next byte]) + byte[] data = { 0x1F }; + AsnReader reader = new AsnReader(data, AsnEncodingRules.BER); + + try + { + reader.PeekTag(); + Assert.True(false, "CryptographicException was thrown"); + } + catch (CryptographicException) + { + } + } + + [Fact] + public static void PeekEncodedValue_Primitive() + { + const string EncodedContents = "010203040506"; + const string EncodedValue = "0406" + EncodedContents; + + byte[] data = (EncodedValue + "0500").HexToByteArray(); + + AsnReader reader = new AsnReader(data, AsnEncodingRules.BER); + Assert.Equal(EncodedValue, reader.PeekEncodedValue().ByteArrayToHex()); + + // It's Peek, so it's reproducible. + Assert.Equal(EncodedValue, reader.PeekEncodedValue().ByteArrayToHex()); + } + + [Fact] + public static void PeekEncodedValue_Indefinite() + { + const string EncodedContents = "040101" + "04050203040506"; + const string EncodedValue = "2480" + EncodedContents + "0000"; + + byte[] data = (EncodedValue + "0500").HexToByteArray(); + + AsnReader reader = new AsnReader(data, AsnEncodingRules.BER); + Assert.Equal(EncodedValue, reader.PeekEncodedValue().ByteArrayToHex()); + + // It's Peek, so it's reproducible. + Assert.Equal(EncodedValue, reader.PeekEncodedValue().ByteArrayToHex()); + } + + [Fact] + public static void PeekEncodedValue_Corrupt_Throws() + { + const string EncodedContents = "040101" + "04050203040506"; + // Constructed bit isn't set, so indefinite length is invalid. + const string EncodedValue = "0480" + EncodedContents + "0000"; + + byte[] data = (EncodedValue + "0500").HexToByteArray(); + + Assert.Throws<CryptographicException>( + () => + { + AsnReader reader = new AsnReader(data, AsnEncodingRules.BER); + reader.PeekEncodedValue(); + }); + } + + [Fact] + public static void PeekContentSpan_Primitive() + { + const string EncodedContents = "010203040506"; + const string EncodedValue = "0406" + EncodedContents; + + byte[] data = (EncodedValue + "0500").HexToByteArray(); + + AsnReader reader = new AsnReader(data, AsnEncodingRules.BER); + Assert.Equal(EncodedContents, reader.PeekContentBytes().ByteArrayToHex()); + + // It's Peek, so it's reproducible. + Assert.Equal(EncodedValue, reader.PeekEncodedValue().ByteArrayToHex()); + } + + [Fact] + public static void PeekContentSpan_Indefinite() + { + const string EncodedContents = "040101" + "04050203040506"; + const string EncodedValue = "2480" + EncodedContents + "0000"; + + byte[] data = (EncodedValue + "0500").HexToByteArray(); + + AsnReader reader = new AsnReader(data, AsnEncodingRules.BER); + Assert.Equal(EncodedContents, reader.PeekContentBytes().ByteArrayToHex()); + + // It's Peek, so it's reproducible. + Assert.Equal(EncodedValue, reader.PeekEncodedValue().ByteArrayToHex()); + } + + [Fact] + public static void PeekContentSpan_Corrupt_Throws() + { + const string EncodedContents = "040101" + "04050203040506"; + // Constructed bit isn't set, so indefinite length is invalid. + const string EncodedValue = "0480" + EncodedContents + "0000"; + + byte[] data = (EncodedValue + "0500").HexToByteArray(); + + Assert.Throws<CryptographicException>( + () => + { + AsnReader reader = new AsnReader(data, AsnEncodingRules.BER); + reader.PeekContentBytes(); + }); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void PeekContentSpan_ExtremelyNested(bool fullArray) + { + byte[] dataBytes = new byte[4 * 16384]; + + // For a full array this will build 2^14 nested indefinite length values. + // PeekContentBytes should return dataBytes.Slice(2, dataBytes.Length - 4) + // + // For what it's worth, the initial algorithm succeeded at 1650, and StackOverflowed with 1651. + // + // With the counter-and-no-recursion algorithm a nesting depth of 534773759 was verified, + // at a cost of 10 minutes of execution and a 2139095036 byte array. + // (The size was "a little bit less than int.MaxValue, since that OOMed my 32-bit process") + int end = dataBytes.Length / 2; + int expectedLength = dataBytes.Length - 4; + + if (!fullArray) + { + // Use 3/4 of what's available, just to prove we're not counting from the end. + // So with "full" being a nesting value 16384 this will use 12288 + end = end / 4 * 3; + expectedLength = 2 * end - 4; + } + + for (int i = 0; i < end; i += 2) + { + // Context-Specific 0 [Constructed] + dataBytes[i] = 0xA0; + // Indefinite length + dataBytes[i + 1] = 0x80; + } + + AsnReader reader = new AsnReader(dataBytes, AsnEncodingRules.BER); + ReadOnlyMemory<byte> contents = reader.PeekContentBytes(); + Assert.Equal(expectedLength, contents.Length); + Assert.True(Unsafe.AreSame(ref dataBytes[2], ref contents.Span.DangerousGetPinnableReference())); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void PeekEncodedValue_ExtremelyNested(bool fullArray) + { + byte[] dataBytes = new byte[4 * 16384]; + + // For a full array this will build 2^14 nested indefinite length values. + // PeekEncodedValue should return the whole array. + int end = dataBytes.Length / 2; + int expectedLength = dataBytes.Length; + + if (!fullArray) + { + // Use 3/4 of what's available, just to prove we're not counting from the end. + // So with "full" being a nesting value 16384 this will use 12288, and + // PeekEncodedValue should give us back 48k, not 64k. + end = end / 4 * 3; + expectedLength = 2 * end; + } + + for (int i = 0; i < end; i += 2) + { + // Context-Specific 0 [Constructed] + dataBytes[i] = 0xA0; + // Indefinite length + dataBytes[i + 1] = 0x80; + } + + AsnReader reader = new AsnReader(dataBytes, AsnEncodingRules.BER); + ReadOnlyMemory<byte> contents = reader.PeekEncodedValue(); + Assert.Equal(expectedLength, contents.Length); + Assert.True(Unsafe.AreSame(ref dataBytes[0], ref contents.Span.DangerousGetPinnableReference())); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadBMPString.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadBMPString.cs new file mode 100644 index 0000000000..c8e8c42cea --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadBMPString.cs @@ -0,0 +1,755 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadBMPString : Asn1ReaderTests + { + public static IEnumerable<object[]> ValidEncodingData { get; } = + new object[][] + { + new object[] + { + PublicEncodingRules.BER, + "1E1A004A006F0068006E00200051002E00200053006D006900740068", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.CER, + "1E1A004A006F0068006E00200051002E00200053006D006900740068", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.DER, + "1E1A004A006F0068006E00200051002E00200053006D006900740068", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.BER, + "3E80" + "041A004A006F0068006E00200051002E00200053006D006900740068" + "0000", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.BER, + "3E1C" + "041A004A006F0068006E00200051002E00200053006D006900740068", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.BER, + "1E00", + "", + }, + new object[] + { + PublicEncodingRules.CER, + "1E00", + "", + }, + new object[] + { + PublicEncodingRules.DER, + "1E00", + "", + }, + new object[] + { + PublicEncodingRules.BER, + "3E00", + "", + }, + new object[] + { + PublicEncodingRules.BER, + "3E80" + "0000", + "", + }, + new object[] + { + PublicEncodingRules.BER, + "3E80" + + "2480" + + // "Dr." + "040600440072002E" + + // " & " + "0406002000260020" + + // "Mrs." + "0408004D00720073002E" + + "0000" + + // " " + "04020020" + + "2480" + + "2410" + + // "Smith" + "040A0053006D006900740068" + + // hyphen (U+2010) + "04022010" + + "0000" + + // "Jones" + "040A004A006F006E00650073" + + "2480" + + // " " + "04020020" + + "2480" + + // The next two bytes are U+FE60, small ampersand + // Since UCS-2 would always chunk evenly under CER the odds of + // misaligned data are low in reality, but maybe some BER encoder + // chunks odd, so a split scenario could still happen. + "0401FE" + + "040160" + + "0000" + + // " " + "04020020" + + // "children" + "0410006300680069006C006400720065006E" + + "0000" + + "0000", + "Dr. & Mrs. Smith\u2010Jones \uFE60 children", + }, + }; + + [Theory] + [MemberData(nameof(ValidEncodingData))] + public static void GetBMPString_Success( + PublicEncodingRules ruleSet, + string inputHex, + string expectedValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + string value = reader.GetCharacterString(UniversalTagNumber.BMPString); + + Assert.Equal(expectedValue, value); + } + + [Theory] + [MemberData(nameof(ValidEncodingData))] + public static void TryCopyBMPString( + PublicEncodingRules ruleSet, + string inputHex, + string expectedValue) + { + byte[] inputData = inputHex.HexToByteArray(); + char[] output = new char[expectedValue.Length]; + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + bool copied; + int charsWritten; + + if (output.Length > 0) + { + output[0] = 'a'; + + copied = reader.TryCopyBMPString( + output.AsSpan().Slice(0, expectedValue.Length - 1), + out charsWritten); + + Assert.False(copied, "reader.TryCopyBMPString - too short"); + Assert.Equal(0, charsWritten); + Assert.Equal('a', output[0]); + } + + copied = reader.TryCopyBMPString( + output, + out charsWritten); + + Assert.True(copied, "reader.TryCopyBMPString"); + + string actualValue = new string(output, 0, charsWritten); + Assert.Equal(expectedValue, actualValue); + } + + [Theory] + [MemberData(nameof(ValidEncodingData))] + public static void TryCopyBMPStringBytes( + PublicEncodingRules ruleSet, + string inputHex, + string expectedString) + { + byte[] inputData = inputHex.HexToByteArray(); + string expectedHex = Text.Encoding.BigEndianUnicode.GetBytes(expectedString).ByteArrayToHex(); + byte[] output = new byte[expectedHex.Length / 2]; + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + bool copied; + int bytesWritten; + + if (output.Length > 0) + { + output[0] = 32; + + copied = reader.TryCopyBMPStringBytes(output.AsSpan().Slice(0, output.Length - 1), + out bytesWritten); + + Assert.False(copied, "reader.TryCopyBMPStringBytes - too short"); + Assert.Equal(0, bytesWritten); + Assert.Equal(32, output[0]); + } + + copied = reader.TryCopyBMPStringBytes(output, + out bytesWritten); + + Assert.True(copied, "reader.TryCopyBMPStringBytes"); + + Assert.Equal( + expectedHex, + new ReadOnlySpan<byte>(output, 0, bytesWritten).ByteArrayToHex()); + + Assert.Equal(output.Length, bytesWritten); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "1E020020", true)] + [InlineData(PublicEncodingRules.BER, "3E80" + "04020020" + "0000", false)] + [InlineData(PublicEncodingRules.BER, "3E04" + "04020020", false)] + public static void TryGetBMPStringBytes( + PublicEncodingRules ruleSet, + string inputHex, + bool expectSuccess) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + bool got = reader.TryGetBMPStringBytes(out ReadOnlyMemory<byte> contents); + + if (expectSuccess) + { + Assert.True(got, "reader.TryGetBMPStringBytes"); + + Assert.True( + Unsafe.AreSame( + ref contents.Span.DangerousGetPinnableReference(), + ref inputData[2])); + } + else + { + Assert.False(got, "reader.TryGetBMPStringBytes"); + Assert.True(contents.IsEmpty, "contents.IsEmpty"); + } + } + + [Theory] + [InlineData("Incomplete Tag", PublicEncodingRules.BER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.CER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.DER, "1F")] + [InlineData("Missing Length", PublicEncodingRules.BER, "1E")] + [InlineData("Missing Length", PublicEncodingRules.CER, "1E")] + [InlineData("Missing Length", PublicEncodingRules.DER, "1E")] + [InlineData("Missing Contents", PublicEncodingRules.BER, "1E02")] + [InlineData("Missing Contents", PublicEncodingRules.CER, "1E02")] + [InlineData("Missing Contents", PublicEncodingRules.DER, "1E02")] + [InlineData("Length Too Long", PublicEncodingRules.BER, "1E0600480069")] + [InlineData("Length Too Long", PublicEncodingRules.CER, "1E0600480069")] + [InlineData("Length Too Long", PublicEncodingRules.DER, "1E0600480069")] + [InlineData("Constructed Form", PublicEncodingRules.DER, "3E0404020049")] + public static void TryGetBMPStringBytes_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => + { + reader.TryGetBMPStringBytes(out ReadOnlyMemory<byte> contents); + }); + } + + [Theory] + [InlineData("Empty", PublicEncodingRules.BER, "")] + [InlineData("Empty", PublicEncodingRules.CER, "")] + [InlineData("Empty", PublicEncodingRules.DER, "")] + [InlineData("Incomplete Tag", PublicEncodingRules.BER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.CER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.DER, "1F")] + [InlineData("Missing Length", PublicEncodingRules.BER, "1E")] + [InlineData("Missing Length", PublicEncodingRules.CER, "1E")] + [InlineData("Missing Length", PublicEncodingRules.DER, "1E")] + [InlineData("Missing Contents", PublicEncodingRules.BER, "1E02")] + [InlineData("Missing Contents", PublicEncodingRules.CER, "1E02")] + [InlineData("Missing Contents", PublicEncodingRules.DER, "1E02")] + [InlineData("Missing Contents - Constructed", PublicEncodingRules.BER, "3E02")] + [InlineData("Missing Contents - Constructed Indef", PublicEncodingRules.BER, "3E80")] + [InlineData("Missing Contents - Constructed Indef", PublicEncodingRules.CER, "3E80")] + [InlineData("Length Too Long", PublicEncodingRules.BER, "1E034869")] + [InlineData("Length Too Long", PublicEncodingRules.CER, "1E034869")] + [InlineData("Length Too Long", PublicEncodingRules.DER, "1E034869")] + [InlineData("Definite Constructed Form", PublicEncodingRules.CER, "3E03040149")] + [InlineData("Definite Constructed Form", PublicEncodingRules.DER, "3E03040149")] + [InlineData("Indefinite Constructed Form - Short Payload", PublicEncodingRules.CER, "3E800401490000")] + [InlineData("Indefinite Constructed Form", PublicEncodingRules.DER, "3E800401490000")] + [InlineData("No nested content", PublicEncodingRules.CER, "3E800000")] + [InlineData("No EoC", PublicEncodingRules.BER, "3E80" + "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.BER, "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.CER, "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.DER, "04024869")] + [InlineData("Wrong Tag - Constructed", PublicEncodingRules.BER, "240404024869")] + [InlineData("Wrong Tag - Constructed Indef", PublicEncodingRules.BER, "2480" + "04024869" + "0000")] + [InlineData("Wrong Tag - Constructed Indef", PublicEncodingRules.CER, "2480" + "04024869" + "0000")] + [InlineData("Wrong Tag - Constructed", PublicEncodingRules.DER, "240404024869")] + [InlineData("Nested Bad Tag", PublicEncodingRules.BER, "3E04" + "1E024869")] + [InlineData("Nested context-specific", PublicEncodingRules.BER, "3E04800400FACE")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.BER, "3E80800400FACE0000")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.CER, "3E80800400FACE0000")] + [InlineData("Nested Length Too Long", PublicEncodingRules.BER, "3E07" + ("2402" + "0404") + "04020049")] + [InlineData("Nested Simple Length Too Long", PublicEncodingRules.BER, "3E03" + "040548656C6C6F")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.BER, "3E8020000000")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.CER, "3E8020000000")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.BER, "3E80000100")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.CER, "3E80000100")] + [InlineData("LongLength EndOfContents", PublicEncodingRules.BER, "3E80008100")] + public static void TryCopyBMPStringBytes_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + byte[] outputData = new byte[inputData.Length + 1]; + outputData[0] = 252; + + int bytesWritten = -1; + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => + { + reader.TryCopyBMPStringBytes(outputData, out bytesWritten); + }); + + Assert.Equal(-1, bytesWritten); + Assert.Equal(252, outputData[0]); + } + + private static void TryCopyBMPString_Throws(PublicEncodingRules ruleSet, byte[] inputData) + { + char[] outputData = new char[inputData.Length + 1]; + outputData[0] = 'a'; + + int bytesWritten = -1; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => + { + reader.TryCopyBMPString( + outputData, + out bytesWritten); + }); + + Assert.Equal(-1, bytesWritten); + Assert.Equal('a', outputData[0]); + } + + [Theory] + [InlineData("Incomplete Tag", PublicEncodingRules.BER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.CER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.DER, "1F")] + [InlineData("Missing Length", PublicEncodingRules.BER, "1E")] + [InlineData("Missing Length", PublicEncodingRules.CER, "1E")] + [InlineData("Missing Length", PublicEncodingRules.DER, "1E")] + [InlineData("Missing Contents", PublicEncodingRules.BER, "1E02")] + [InlineData("Missing Contents", PublicEncodingRules.CER, "1E02")] + [InlineData("Missing Contents", PublicEncodingRules.DER, "1E02")] + [InlineData("Length Too Long", PublicEncodingRules.BER, "1E0600480069")] + [InlineData("Length Too Long", PublicEncodingRules.CER, "1E0600480069")] + [InlineData("Length Too Long", PublicEncodingRules.DER, "1E0600480069")] + [InlineData("Constructed Form", PublicEncodingRules.DER, "3E0404020049")] + [InlineData("Bad BMP value (odd length)", PublicEncodingRules.BER, "1E0120")] + [InlineData("Bad BMP value (high surrogate)", PublicEncodingRules.BER, "1E02D800")] + [InlineData("Bad BMP value (high private surrogate)", PublicEncodingRules.BER, "1E02DB81")] + [InlineData("Bad BMP value (low surrogate)", PublicEncodingRules.BER, "1E02DC00")] + [InlineData("Wrong Tag", PublicEncodingRules.BER, "04024869")] + public static void GetBMPString_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => + { + reader.GetCharacterString(UniversalTagNumber.BMPString); + }); + } + + [Theory] + [InlineData("Empty", PublicEncodingRules.BER, "")] + [InlineData("Empty", PublicEncodingRules.CER, "")] + [InlineData("Empty", PublicEncodingRules.DER, "")] + [InlineData("Incomplete Tag", PublicEncodingRules.BER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.CER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.DER, "1F")] + [InlineData("Missing Length", PublicEncodingRules.BER, "1E")] + [InlineData("Missing Length", PublicEncodingRules.CER, "1E")] + [InlineData("Missing Length", PublicEncodingRules.DER, "1E")] + [InlineData("Missing Contents", PublicEncodingRules.BER, "1E02")] + [InlineData("Missing Contents", PublicEncodingRules.CER, "1E02")] + [InlineData("Missing Contents", PublicEncodingRules.DER, "1E02")] + [InlineData("Missing Contents - Constructed", PublicEncodingRules.BER, "3E02")] + [InlineData("Missing Contents - Constructed Indef", PublicEncodingRules.BER, "3E80")] + [InlineData("Missing Contents - Constructed Indef", PublicEncodingRules.CER, "3E80")] + [InlineData("Length Too Long", PublicEncodingRules.BER, "1E034869")] + [InlineData("Length Too Long", PublicEncodingRules.CER, "1E034869")] + [InlineData("Length Too Long", PublicEncodingRules.DER, "1E034869")] + [InlineData("Definite Constructed Form", PublicEncodingRules.CER, "3E03040149")] + [InlineData("Definite Constructed Form", PublicEncodingRules.DER, "3E03040149")] + [InlineData("Indefinite Constructed Form - Short Payload", PublicEncodingRules.CER, "3E800401490000")] + [InlineData("Indefinite Constructed Form", PublicEncodingRules.DER, "3E800401490000")] + [InlineData("No nested content", PublicEncodingRules.CER, "3E800000")] + [InlineData("No EoC", PublicEncodingRules.BER, "3E80" + "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.BER, "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.CER, "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.DER, "04024869")] + [InlineData("Wrong Tag - Constructed", PublicEncodingRules.BER, "240404024869")] + [InlineData("Wrong Tag - Constructed Indef", PublicEncodingRules.BER, "2480" + "04024869" + "0000")] + [InlineData("Wrong Tag - Constructed Indef", PublicEncodingRules.CER, "2480" + "04024869" + "0000")] + [InlineData("Wrong Tag - Constructed", PublicEncodingRules.DER, "240404024869")] + [InlineData("Nested Bad Tag", PublicEncodingRules.BER, "3E04" + "1E024869")] + [InlineData("Nested context-specific", PublicEncodingRules.BER, "3E04800400FACE")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.BER, "3E80800400FACE0000")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.CER, "3E80800400FACE0000")] + [InlineData("Nested Length Too Long", PublicEncodingRules.BER, "3E07" + ("2402" + "0404") + "04020049")] + [InlineData("Nested Simple Length Too Long", PublicEncodingRules.BER, "3E03" + "040548656C6C6F")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.BER, "3E8020000000")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.CER, "3E8020000000")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.BER, "3E80000100")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.CER, "3E80000100")] + [InlineData("LongLength EndOfContents", PublicEncodingRules.BER, "3E80008100")] + [InlineData("Bad BMP value (odd length)", PublicEncodingRules.BER, "1E0120")] + [InlineData("Bad BMP value (high surrogate)", PublicEncodingRules.BER, "1E02D800")] + [InlineData("Bad BMP value (high private surrogate)", PublicEncodingRules.BER, "1E02DB81")] + [InlineData("Bad BMP value (low surrogate)", PublicEncodingRules.BER, "1E02DC00")] + public static void TryCopyBMPString_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + TryCopyBMPString_Throws(ruleSet, inputData); + } + + [Fact] + public static void TryCopyBMPString_Throws_CER_NestedTooLong() + { + // CER says that the maximum encoding length for a BMPString primitive + // is 1000. + // + // This test checks it for a primitive contained within a constructed. + // + // So we need 04 [1001] { 1001 0x00s } + // 1001 => 0x3E9, so the length encoding is 82 03 E9. + // 1001 + 3 + 1 == 1005 + // + // Plus a leading 3E 80 (indefinite length constructed) + // and a trailing 00 00 (End of contents) + // == 1009 + byte[] input = new byte[1009]; + // CONSTRUCTED BMPSTRING (indefinite) + input[0] = 0x3E; + input[1] = 0x80; + // OCTET STRING (1001) + input[2] = 0x04; + input[3] = 0x82; + input[4] = 0x03; + input[5] = 0xE9; + // EOC implicit since the byte[] initializes to zeros + + TryCopyBMPString_Throws(PublicEncodingRules.CER, input); + } + + [Fact] + public static void TryCopyBMPString_Throws_CER_NestedTooShortIntermediate() + { + // CER says that the maximum encoding length for a BMPString primitive + // is 1000, and in the constructed form the lengths must be + // [ 1000, 1000, 1000, ..., len%1000 ] + // + // So 1000, 2, 2 is illegal. + // + // 3E 80 (indefinite constructed BMP string) + // 04 82 03 08 (octet string, 1000 bytes) + // [1000 content bytes] + // 04 02 (octet string, 2 bytes) + // [2 content bytes] + // 04 02 (octet string, 2 bytes) + // [2 content bytes] + // 00 00 (end of contents) + // Looks like 1,016 bytes. + byte[] input = new byte[1016]; + // CONSTRUCTED BMP STRING (indefinite) + input[0] = 0x3E; + input[1] = 0x80; + // OCTET STRING (1000) + input[2] = 0x03; + input[3] = 0x82; + input[4] = 0x03; + input[5] = 0xE8; + // OCTET STRING (2) + input[1006] = 0x04; + input[1007] = 0x02; + // OCTET STRING (2) + input[1010] = 0x04; + input[1011] = 0x02; + // EOC implicit since the byte[] initializes to zeros + + TryCopyBMPString_Throws(PublicEncodingRules.CER, input); + } + + [Fact] + public static void TryCopyBMPStringBytes_Success_CER_MaxPrimitiveLength() + { + // CER says that the maximum encoding length for a BMPString primitive + // is 1000. + // + // So we need 1E [1000] { 1000 anythings } + // 1000 => 0x3E8, so the length encoding is 82 03 E8. + // 1000 + 3 + 1 == 1004 + byte[] input = new byte[1004]; + input[0] = 0x1E; + input[1] = 0x82; + input[2] = 0x03; + input[3] = 0xE8; + + // Content + input[4] = 0x65; + input[5] = 0x65; + input[1002] = 0x61; + input[1003] = 0x61; + + byte[] output = new byte[1000]; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + bool success = reader.TryCopyBMPStringBytes(output, out int bytesWritten); + + Assert.True(success, "reader.TryCopyBMPStringBytes"); + Assert.Equal(1000, bytesWritten); + + Assert.Equal( + input.AsReadOnlySpan().Slice(4).ByteArrayToHex(), + output.ByteArrayToHex()); + } + + [Fact] + public static void TryCopyBMPStringBytes_Success_CER_MinConstructedLength() + { + // CER says that the maximum encoding length for a BMPString primitive + // is 1000, and that a constructed form must be used for values greater + // than 1000 bytes, with segments dividing up for each thousand + // [1000, 1000, ..., len%1000]. + // + // So our smallest constructed form is 1001 bytes, [1000, 1] + // + // 3E 80 (indefinite constructed BMPString) + // 04 82 03 E9 (primitive octet string, 1000 bytes) + // [1000 content bytes] + // 04 01 (primitive octet string, 1 byte) + // pp + // 00 00 (end of contents, 0 bytes) + // 1011 total. + byte[] input = new byte[1011]; + int offset = 0; + // CONSTRUCTED BMPSTRING (Indefinite) + input[offset++] = 0x3E; + input[offset++] = 0x80; + // OCTET STRING (1000) + input[offset++] = 0x04; + input[offset++] = 0x82; + input[offset++] = 0x03; + input[offset++] = 0xE8; + + // Primitive 1: (65 65 :: 61 61) (1000) + input[offset++] = 0x65; + input[offset] = 0x65; + offset += 997; + input[offset++] = 0x61; + input[offset++] = 0x61; + + // OCTET STRING (1) + input[offset++] = 0x04; + input[offset++] = 0x01; + + // Primitive 2: One more byte + input[offset] = 0x2E; + + byte[] expected = new byte[1001]; + offset = 0; + expected[offset++] = 0x65; + expected[offset] = 0x65; + offset += 997; + expected[offset++] = 0x61; + expected[offset++] = 0x61; + expected[offset] = 0x2E; + + byte[] output = new byte[1001]; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + bool success = reader.TryCopyBMPStringBytes(output, out int bytesWritten); + + Assert.True(success, "reader.TryCopyBMPStringBytes"); + Assert.Equal(1001, bytesWritten); + + Assert.Equal( + expected.ByteArrayToHex(), + output.ByteArrayToHex()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x1E, 4, 0, (byte)'h', 0, (byte)'i' }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.TryGetBMPStringBytes(Asn1Tag.Null, out _)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetBMPStringBytes(new Asn1Tag(TagClass.ContextSpecific, 0), out _)); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + Assert.True(reader.TryGetBMPStringBytes(out ReadOnlyMemory<byte> value)); + Assert.Equal("00680069", value.ByteArrayToHex()); + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x87, 2, 0x20, 0x10 }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.TryGetBMPStringBytes(Asn1Tag.Null, out _)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.TryGetBMPStringBytes(out _)); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetBMPStringBytes(new Asn1Tag(TagClass.Application, 0), out _)); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetBMPStringBytes(new Asn1Tag(TagClass.ContextSpecific, 1), out _)); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + Assert.True( + reader.TryGetBMPStringBytes( + new Asn1Tag(TagClass.ContextSpecific, 7), + out ReadOnlyMemory<byte> value)); + + Assert.Equal("2010", value.ByteArrayToHex()); + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "1E022010", PublicTagClass.Universal, 30)] + [InlineData(PublicEncodingRules.CER, "1E022010", PublicTagClass.Universal, 30)] + [InlineData(PublicEncodingRules.DER, "1E022010", PublicTagClass.Universal, 30)] + [InlineData(PublicEncodingRules.BER, "8002FE60", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C02FE60", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A4602FE60", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.True( + reader.TryGetBMPStringBytes( + new Asn1Tag((TagClass)tagClass, tagValue, true), + out ReadOnlyMemory<byte> val1)); + + Assert.False(reader.HasData); + + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.True( + reader.TryGetBMPStringBytes( + new Asn1Tag((TagClass)tagClass, tagValue, false), + out ReadOnlyMemory<byte> val2)); + + Assert.False(reader.HasData); + + Assert.Equal(val1.ByteArrayToHex(), val2.ByteArrayToHex()); + } + } + + internal static class ReaderBMPExtensions + { + public static bool TryGetBMPStringBytes( + this AsnReader reader, + out ReadOnlyMemory<byte> contents) + { + return reader.TryGetPrimitiveCharacterStringBytes( + UniversalTagNumber.BMPString, + out contents); + } + + public static bool TryGetBMPStringBytes( + this AsnReader reader, + Asn1Tag expectedTag, + out ReadOnlyMemory<byte> contents) + { + return reader.TryGetPrimitiveCharacterStringBytes( + expectedTag, + UniversalTagNumber.BMPString, + out contents); + } + + public static bool TryCopyBMPStringBytes( + this AsnReader reader, + Span<byte> destination, + out int bytesWritten) + { + return reader.TryCopyCharacterStringBytes( + UniversalTagNumber.BMPString, + destination, + out bytesWritten); + } + + public static bool TryCopyBMPString( + this AsnReader reader, + Span<char> destination, + out int charsWritten) + { + return reader.TryCopyCharacterString( + UniversalTagNumber.BMPString, + destination, + out charsWritten); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadBitString.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadBitString.cs new file mode 100644 index 0000000000..2455c2efcb --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadBitString.cs @@ -0,0 +1,671 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadBitString : Asn1ReaderTests + { + [Theory] + [InlineData("Uncleared unused bit", PublicEncodingRules.BER, "030201FF")] + [InlineData("Constructed Payload", PublicEncodingRules.BER, "2302030100")] + [InlineData("Constructed Payload-Indefinite", PublicEncodingRules.BER, "238003010000")] + // This value is actually invalid CER, but it returns false since it's not primitive and + // it isn't worth preempting the descent to find out it was invalid. + [InlineData("Constructed Payload-Indefinite", PublicEncodingRules.CER, "238003010000")] + public static void TryGetBitStringBytes_Fails( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryGetPrimitiveBitStringValue( + out int unusedBitCount, + out ReadOnlyMemory<byte> contents); + + Assert.False(didRead, "reader.TryGetBitStringBytes"); + Assert.Equal(0, unusedBitCount); + Assert.Equal(0, contents.Length); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, 0, 0, "030100")] + [InlineData(PublicEncodingRules.BER, 1, 1, "030201FE")] + [InlineData(PublicEncodingRules.CER, 2, 4, "030502FEEFF00C")] + [InlineData(PublicEncodingRules.DER, 7, 1, "03020780")] + [InlineData(PublicEncodingRules.DER, 0, 4, "030500FEEFF00D" + "0500")] + public static void TryGetBitStringBytes_Success( + PublicEncodingRules ruleSet, + int expectedUnusedBitCount, + int expectedLength, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryGetPrimitiveBitStringValue( + out int unusedBitCount, + out ReadOnlyMemory<byte> contents); + + Assert.True(didRead, "reader.TryGetBitStringBytes"); + Assert.Equal(expectedUnusedBitCount, unusedBitCount); + Assert.Equal(expectedLength, contents.Length); + } + + [Theory] + [InlineData("Wrong Tag", PublicEncodingRules.BER, "0500")] + [InlineData("Wrong Tag", PublicEncodingRules.CER, "0500")] + [InlineData("Wrong Tag", PublicEncodingRules.DER, "0500")] + [InlineData("Zero Length", PublicEncodingRules.BER, "0300")] + [InlineData("Zero Length", PublicEncodingRules.CER, "0300")] + [InlineData("Zero Length", PublicEncodingRules.DER, "0300")] + [InlineData("Bad Length", PublicEncodingRules.BER, "030200")] + [InlineData("Bad Length", PublicEncodingRules.CER, "030200")] + [InlineData("Bad Length", PublicEncodingRules.DER, "030200")] + [InlineData("Constructed Form", PublicEncodingRules.DER, "2303030100")] + public static void TryGetBitStringBytes_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => + { + reader.TryGetPrimitiveBitStringValue( + out int unusedBitCount, + out ReadOnlyMemory<byte> contents); + }); + } + + [Fact] + public static void TryGetBitStringBytes_Throws_CER_TooLong() + { + // CER says that the maximum encoding length for a BitString primitive is + // 1000 (999 value bytes and 1 unused bit count byte). + // + // So we need 03 [1001] { 1001 0x00s } + // 1001 => 0x3E9, so the length encoding is 82 03 E9. + // 1001 + 3 + 1 == 1005 + byte[] input = new byte[1005]; + input[0] = 0x03; + input[1] = 0x82; + input[2] = 0x03; + input[3] = 0xE9; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + + Assert.Throws<CryptographicException>( + () => + { + reader.TryGetPrimitiveBitStringValue( + out int unusedBitCount, + out ReadOnlyMemory<byte> contents); + }); + } + + [Fact] + public static void TryGetBitStringBytes_Success_CER_MaxLength() + { + // CER says that the maximum encoding length for a BitString primitive is + // 1000 (999 value bytes and 1 unused bit count byte). + // + // So we need 03 [1000] [0x00-0x07] { 998 anythings } [a byte that's legal for the bitmask] + // 1000 => 0x3E8, so the length encoding is 82 03 E8. + // 1000 + 3 + 1 == 1004 + byte[] input = new byte[1004]; + input[0] = 0x03; + input[1] = 0x82; + input[2] = 0x03; + input[3] = 0xE8; + + // Unused bits + input[4] = 0x02; + + // Payload + input[5] = 0xA0; + input[1002] = 0xA5; + input[1003] = 0xFC; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + + bool success = reader.TryGetPrimitiveBitStringValue( + out int unusedBitCount, + out ReadOnlyMemory<byte> contents); + + Assert.True(success, "reader.TryGetBitStringBytes"); + Assert.Equal(input[4], unusedBitCount); + Assert.Equal(999, contents.Length); + + // Check that it is, in fact, the same memory. No copies with this API. + Assert.True( + Unsafe.AreSame( + ref contents.Span.DangerousGetPinnableReference(), + ref input[5])); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "03020780")] + [InlineData(PublicEncodingRules.BER, "030207FF")] + [InlineData(PublicEncodingRules.CER, "03020780")] + [InlineData(PublicEncodingRules.DER, "03020780")] + [InlineData( + PublicEncodingRules.BER, + "2380" + + "2380" + + "0000" + + "03020000" + + "0000")] + public static void TryCopyBitStringBytes_Fails(PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryCopyBitStringBytes( + Span<byte>.Empty, + out int unusedBitCount, + out int bytesWritten); + + Assert.False(didRead, "reader.TryCopyBitStringBytes"); + Assert.Equal(0, unusedBitCount); + Assert.Equal(0, bytesWritten); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "03020780", "80", 7)] + [InlineData(PublicEncodingRules.BER, "030207FF", "80", 7)] + [InlineData(PublicEncodingRules.CER, "03020780", "80", 7)] + [InlineData(PublicEncodingRules.DER, "03020680", "80", 6)] + [InlineData(PublicEncodingRules.BER, "23800000", "", 0)] + [InlineData(PublicEncodingRules.BER, "2300", "", 0)] + [InlineData(PublicEncodingRules.BER, "2300" + "0500", "", 0)] + [InlineData(PublicEncodingRules.BER, "0303010203" + "0500", "0202", 1)] + [InlineData( + PublicEncodingRules.BER, + "2380" + + "2380" + + "0000" + + "03020000" + + "0000", + "00", + 0)] + [InlineData( + PublicEncodingRules.BER, + "230C" + + "2380" + + "2380" + + "0000" + + "03020000" + + "0000", + "00", + 0)] + [InlineData( + PublicEncodingRules.BER, + "2380" + + "2308" + + "030200FA" + + "030200CE" + + "2380" + + "2380" + + "2380" + + "030300F00D" + + "0000" + + "0000" + + "03020001" + + "0000" + + "0303000203" + + "030203FF" + + "2380" + + "0000" + + "0000", + "FACEF00D010203F8", + 3)] + public static void TryCopyBitStringBytes_Success( + PublicEncodingRules ruleSet, + string inputHex, + string expectedHex, + int expectedUnusedBitCount) + { + byte[] inputData = inputHex.HexToByteArray(); + byte[] output = new byte[expectedHex.Length / 2]; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryCopyBitStringBytes( + output, + out int unusedBitCount, + out int bytesWritten); + + Assert.True(didRead, "reader.TryCopyBitStringBytes"); + Assert.Equal(expectedUnusedBitCount, unusedBitCount); + Assert.Equal(expectedHex, output.AsReadOnlySpan().Slice(0, bytesWritten).ByteArrayToHex()); + } + + private static void TryCopyBitStringBytes_Throws( + PublicEncodingRules ruleSet, + byte[] input) + { + AsnReader reader = new AsnReader(input, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => + { + reader.TryCopyBitStringBytes( + Span<byte>.Empty, + out int unusedBitCount, + out int bytesWritten); + }); + } + + [Theory] + [InlineData("Wrong Tag", PublicEncodingRules.BER, "0500")] + [InlineData("Wrong Tag", PublicEncodingRules.CER, "0500")] + [InlineData("Wrong Tag", PublicEncodingRules.DER, "0500")] + [InlineData("Zero Length", PublicEncodingRules.BER, "0300")] + [InlineData("Zero Length", PublicEncodingRules.CER, "0300")] + [InlineData("Zero Length", PublicEncodingRules.DER, "0300")] + [InlineData("Bad Length", PublicEncodingRules.BER, "030200")] + [InlineData("Bad Length", PublicEncodingRules.CER, "030200")] + [InlineData("Bad Length", PublicEncodingRules.DER, "030200")] + [InlineData("Constructed Form", PublicEncodingRules.DER, "2303030100")] + [InlineData("Bad unused bits", PublicEncodingRules.BER, "03020800")] + [InlineData("Bad unused bits", PublicEncodingRules.CER, "03020800")] + [InlineData("Bad unused bits", PublicEncodingRules.DER, "03020800")] + [InlineData("Bad unused bits-nodata", PublicEncodingRules.BER, "030101")] + [InlineData("Bad unused bits-nodata", PublicEncodingRules.CER, "030101")] + [InlineData("Bad unused bits-nodata", PublicEncodingRules.DER, "030101")] + [InlineData("Bad nested unused bits", PublicEncodingRules.BER, "230403020800")] + [InlineData("Bad nested unused bits-indef", PublicEncodingRules.BER, "2380030208000000")] + [InlineData("Bad nested unused bits-indef", PublicEncodingRules.CER, "2380030208000000")] + [InlineData("Bad nested unused bits-nodata", PublicEncodingRules.BER, "2303030101")] + [InlineData("Bad nested unused bits-nodata-indef", PublicEncodingRules.BER, "23800301010000")] + [InlineData("Bad nested unused bits-nodata-indef", PublicEncodingRules.CER, "23800301010000")] + [InlineData("Bad mask", PublicEncodingRules.CER, "030201FF")] + [InlineData("Bad mask", PublicEncodingRules.DER, "030201FF")] + [InlineData("Bad nested mask", PublicEncodingRules.CER, "2380030201FF0000")] + [InlineData("Nested context-specific", PublicEncodingRules.BER, "2304800300FACE")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.BER, "2380800300FACE0000")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.CER, "2380800300FACE0000")] + [InlineData("Nested boolean", PublicEncodingRules.BER, "2303010100")] + [InlineData("Nested boolean (indef)", PublicEncodingRules.BER, "23800101000000")] + [InlineData("Nested boolean (indef)", PublicEncodingRules.CER, "23800101000000")] + [InlineData("Nested constructed form", PublicEncodingRules.CER, "2380" + "2380" + "03010" + "000000000")] + [InlineData("No terminator", PublicEncodingRules.BER, "2380" + "03020000" + "")] + [InlineData("No terminator", PublicEncodingRules.CER, "2380" + "03020000" + "")] + [InlineData("No content", PublicEncodingRules.BER, "2380")] + [InlineData("No content", PublicEncodingRules.CER, "2380")] + [InlineData("No nested content", PublicEncodingRules.CER, "23800000")] + [InlineData("Nested value too long", PublicEncodingRules.BER, "2380030A00")] + [InlineData("Nested value too long - constructed", PublicEncodingRules.BER, "2380230A00")] + [InlineData("Nested value too long - simple", PublicEncodingRules.BER, "2303" + "03050000000000")] + [InlineData( + "Unused bits in intermediate segment", + PublicEncodingRules.BER, + "2380" + + "0303000102" + + "0303020304" + + "0303010506" + + "0000")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.BER, "238020000000")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.CER, "238020000000")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.BER, "2380000100")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.CER, "2380000100")] + [InlineData("LongLength EndOfContents", PublicEncodingRules.BER, "2380008100")] + [InlineData("Constructed Payload-TooShort", PublicEncodingRules.CER, "23800301000000")] + public static void TryCopyBitStringBytes_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + TryCopyBitStringBytes_Throws(ruleSet, inputData); + } + + [Fact] + public static void TryCopyBitStringBytes_Throws_CER_TooLong() + { + // CER says that the maximum encoding length for a BitString primitive is + // 1000 (999 value bytes and 1 unused bit count byte). + // + // So we need 03 [1001] { 1001 0x00s } + // 1001 => 0x3E9, so the length encoding is 82 03 E9. + // 1001 + 3 + 1 == 1004 + byte[] input = new byte[1004]; + input[0] = 0x03; + input[1] = 0x82; + input[2] = 0x03; + input[3] = 0xE9; + + TryCopyBitStringBytes_Throws(PublicEncodingRules.CER, input); + } + + [Fact] + public static void TryCopyBitStringBytes_Throws_CER_NestedTooLong() + { + // CER says that the maximum encoding length for a BitString primitive is + // 1000 (999 value bytes and 1 unused bit count byte). + // + // This test checks it for a primitive contained within a constructed. + // + // So we need 03 [1001] { 1001 0x00s } + // 1001 => 0x3E9, so the length encoding is 82 03 E9. + // 1001 + 3 + 1 == 1005 + // + // Plus a leading 23 80 (indefinite length constructed) + // and a trailing 00 00 (End of contents) + // == 1009 + byte[] input = new byte[1009]; + // CONSTRUCTED BIT STRING (indefinite) + input[0] = 0x23; + input[1] = 0x80; + // BIT STRING (1001) + input[2] = 0x03; + input[3] = 0x82; + input[4] = 0x03; + input[5] = 0xE9; + // EOC implicit since the byte[] initializes to zeros + + TryCopyBitStringBytes_Throws(PublicEncodingRules.CER, input); + } + + [Fact] + public static void TryCopyBitStringBytes_Throws_CER_NestedTooShortIntermediate() + { + // CER says that the maximum encoding length for a BitString primitive is + // 1000 (999 value bytes and 1 unused bit count byte), and in the constructed + // form the lengths must be + // [ 1000, 1000, 1000, ..., len%1000 ] + // + // So 1000, 2, 2 is illegal. + // + // 23 80 (indefinite constructed bit string) + // 03 82 03 08 (bit string, 1000 bytes) + // [1000 content bytes] + // 03 02 (bit string, 2 bytes) + // [2 content bytes] + // 03 02 (bit string, 2 bytes) + // [2 content bytes] + // 00 00 (end of contents) + // Looks like 1,016 bytes. + byte[] input = new byte[1016]; + // CONSTRUCTED BIT STRING (indefinite) + input[0] = 0x23; + input[1] = 0x80; + // BIT STRING (1000) + input[2] = 0x03; + input[3] = 0x82; + input[4] = 0x03; + input[5] = 0xE8; + // BIT STRING (2) + input[1006] = 0x03; + input[1007] = 0x02; + // BIT STRING (2) + input[1010] = 0x03; + input[1011] = 0x02; + // EOC implicit since the byte[] initializes to zeros + + TryCopyBitStringBytes_Throws(PublicEncodingRules.CER, input); + } + + [Fact] + public static void TryCopyBitStringBytes_Success_CER_MaxPrimitiveLength() + { + // CER says that the maximum encoding length for a BitString primitive is + // 1000 (999 value bytes and 1 unused bit count byte). + // + // So we need 03 [1000] [0x00-0x07] { 998 anythings } [a byte that's legal for the bitmask] + // 1000 => 0x3E8, so the length encoding is 82 03 E8. + // 1000 + 3 + 1 == 1003 + byte[] input = new byte[1004]; + input[0] = 0x03; + input[1] = 0x82; + input[2] = 0x03; + input[3] = 0xE8; + + // Unused bits + input[4] = 0x02; + + // Payload + input[5] = 0xA0; + input[1002] = 0xA5; + input[1003] = 0xFC; + + byte[] output = new byte[999]; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + + bool success = reader.TryCopyBitStringBytes( + output, + out int unusedBitCount, + out int bytesWritten); + + Assert.True(success, "reader.TryCopyBitStringBytes"); + Assert.Equal(input[4], unusedBitCount); + Assert.Equal(999, bytesWritten); + + Assert.Equal( + input.AsReadOnlySpan().Slice(5).ByteArrayToHex(), + output.ByteArrayToHex()); + } + + [Fact] + public static void TryCopyBitStringBytes_Success_CER_MinConstructedLength() + { + // CER says that the maximum encoding length for a BitString primitive is + // 1000 (999 value bytes and 1 unused bit count byte), and that a constructed + // form must be used for values greater than 1000 bytes, with segments dividing + // up for each thousand [1000, 1000, ..., len%1000]. + // + // Bit string primitives are one byte of "unused bits" and the rest are payload, + // so the minimum constructed payload has total content length 1002: + // [1000 (1+999), 2 (1+1)] + // + // 23 80 (indefinite constructed bit string) + // 03 82 03 E9 (primitive bit string, 1000 bytes) + // 00 [999 more payload bytes] + // 03 02 (primitive bit string, 2 bytes) + // uu pp + // 00 00 (end of contents, 0 bytes) + // 1010 total. + byte[] input = new byte[1012]; + int offset = 0; + // CONSTRUCTED BIT STRING (Indefinite) + input[offset++] = 0x23; + input[offset++] = 0x80; + // BIT STRING (1000) + input[offset++] = 0x03; + input[offset++] = 0x82; + input[offset++] = 0x03; + input[offset++] = 0xE8; + + // Primitive 1: Unused bits MUST be 0. + input[offset++] = 0x00; + + // Payload (A0 :: A5 FC) (999) + input[offset] = 0xA0; + offset += 997; + input[offset++] = 0xA5; + input[offset++] = 0xFC; + + // BIT STRING (2) + input[offset++] = 0x03; + input[offset++] = 0x02; + + // Primitive 2: Unused bits 0-7 + input[offset++] = 0x3; + + // Payload (must have the three least significant bits unset) + input[offset] = 0b0000_1000; + + byte[] expected = new byte[1000]; + offset = 0; + expected[offset] = 0xA0; + offset += 997; + expected[offset++] = 0xA5; + expected[offset++] = 0xFC; + expected[offset] = 0b0000_1000; + + byte[] output = new byte[1000]; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + + bool success = reader.TryCopyBitStringBytes( + output, + out int unusedBitCount, + out int bytesWritten); + + Assert.True(success, "reader.TryCopyBitStringBytes"); + Assert.Equal(input[1006], unusedBitCount); + Assert.Equal(1000, bytesWritten); + + Assert.Equal( + expected.ByteArrayToHex(), + output.ByteArrayToHex()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = { 3, 2, 1, 0x7E }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.TryGetPrimitiveBitStringValue(Asn1Tag.Null, out _, out _)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveBitStringValue(new Asn1Tag(TagClass.ContextSpecific, 0), out _, out _)); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + Assert.True(reader.TryGetPrimitiveBitStringValue(out int unusedBitCount, out ReadOnlyMemory<byte> contents)); + Assert.Equal("7E", contents.ByteArrayToHex()); + Assert.Equal(1, unusedBitCount); + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x87, 2, 0, 0x80 }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.TryGetPrimitiveBitStringValue(Asn1Tag.Null, out _, out _)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.TryGetPrimitiveBitStringValue(out _, out _)); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveBitStringValue(new Asn1Tag(TagClass.Application, 0), out _, out _)); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveBitStringValue(new Asn1Tag(TagClass.ContextSpecific, 1), out _, out _)); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + Assert.True( + reader.TryGetPrimitiveBitStringValue( + new Asn1Tag(TagClass.ContextSpecific, 7), + out int unusedBitCount, + out ReadOnlyMemory<byte> contents)); + + Assert.Equal("80", contents.ByteArrayToHex()); + Assert.Equal(0, unusedBitCount); + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "030400010203", PublicTagClass.Universal, 3)] + [InlineData(PublicEncodingRules.CER, "030400010203", PublicTagClass.Universal, 3)] + [InlineData(PublicEncodingRules.DER, "030400010203", PublicTagClass.Universal, 3)] + [InlineData(PublicEncodingRules.BER, "800200FF", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C0200FF", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A460200FF", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.True( + reader.TryGetPrimitiveBitStringValue( + new Asn1Tag((TagClass)tagClass, tagValue, true), + out int ubc1, + out ReadOnlyMemory<byte> val1)); + + Assert.False(reader.HasData); + + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.True( + reader.TryGetPrimitiveBitStringValue( + new Asn1Tag((TagClass)tagClass, tagValue, false), + out int ubc2, + out ReadOnlyMemory<byte> val2)); + + Assert.False(reader.HasData); + + Assert.Equal(val1.ByteArrayToHex(), val2.ByteArrayToHex()); + Assert.Equal(ubc1, ubc2); + } + + [Fact] + public static void TryCopyBitStringBytes_ExtremelyNested() + { + byte[] dataBytes = new byte[4 * 16384]; + + // This will build 2^14 nested indefinite length values. + // In the end, none of them contain any content. + // + // For what it's worth, the initial algorithm succeeded at 1017, and StackOverflowed with 1018. + int end = dataBytes.Length / 2; + + // UNIVERSAL BIT STRING [Constructed] + const byte Tag = 0x20 | (byte)UniversalTagNumber.BitString; + + for (int i = 0; i < end; i += 2) + { + dataBytes[i] = Tag; + // Indefinite length + dataBytes[i + 1] = 0x80; + } + + AsnReader reader = new AsnReader(dataBytes, AsnEncodingRules.BER); + + int bytesWritten; + int unusedBitCount; + + Assert.True( + reader.TryCopyBitStringBytes(Span<byte>.Empty, out unusedBitCount, out bytesWritten)); + + Assert.Equal(0, bytesWritten); + Assert.Equal(0, unusedBitCount); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadBoolean.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadBoolean.cs new file mode 100644 index 0000000000..6aaa15048e --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadBoolean.cs @@ -0,0 +1,220 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadBoolean : Asn1ReaderTests + { + [Theory] + [InlineData(PublicEncodingRules.BER, false, 3, "010100")] + [InlineData(PublicEncodingRules.BER, true, 3, "010101")] + // Padded length + [InlineData(PublicEncodingRules.BER, true, 4, "01810101")] + [InlineData(PublicEncodingRules.BER, true, 3, "0101FF0500")] + [InlineData(PublicEncodingRules.CER, false, 3, "0101000500")] + [InlineData(PublicEncodingRules.CER, true, 3, "0101FF")] + [InlineData(PublicEncodingRules.DER, false, 3, "010100")] + [InlineData(PublicEncodingRules.DER, true, 3, "0101FF0500")] + // Context Specific 0 + [InlineData(PublicEncodingRules.DER, true, 3, "8001FF0500")] + // Application 31 + [InlineData(PublicEncodingRules.DER, true, 4, "5F1F01FF0500")] + // Private 253 + [InlineData(PublicEncodingRules.CER, false, 5, "DF817D01000500")] + public static void ReadBoolean_Success( + PublicEncodingRules ruleSet, + bool expectedValue, + int expectedBytesRead, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Asn1Tag tag = reader.PeekTag(); + bool value; + + if (tag.TagClass == TagClass.Universal) + { + value = reader.ReadBoolean(); + } + else + { + value = reader.ReadBoolean(tag); + } + + if (inputData.Length == expectedBytesRead) + { + Assert.False(reader.HasData, "reader.HasData"); + } + else + { + Assert.True(reader.HasData, "reader.HasData"); + } + + if (expectedValue) + { + Assert.True(value, "value"); + } + else + { + Assert.False(value, "value"); + } + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = { 1, 1, 0 }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.ReadBoolean(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.ReadBoolean(new Asn1Tag(TagClass.ContextSpecific, 0))); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + bool value = reader.ReadBoolean(); + Assert.False(value, "value"); + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x80, 1, 0xFF }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.ReadBoolean(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.ReadBoolean()); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>(() => reader.ReadBoolean(new Asn1Tag(TagClass.Application, 0))); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>(() => reader.ReadBoolean(new Asn1Tag(TagClass.ContextSpecific, 1))); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + bool value = reader.ReadBoolean(new Asn1Tag(TagClass.ContextSpecific, 0)); + Assert.True(value, "value"); + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "0101FF", PublicTagClass.Universal, 1)] + [InlineData(PublicEncodingRules.CER, "0101FF", PublicTagClass.Universal, 1)] + [InlineData(PublicEncodingRules.DER, "0101FF", PublicTagClass.Universal, 1)] + [InlineData(PublicEncodingRules.BER, "8001FF", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C01FF", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A4601FF", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + bool val1 = reader.ReadBoolean(new Asn1Tag((TagClass)tagClass, tagValue, true)); + Assert.False(reader.HasData); + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + bool val2 = reader.ReadBoolean(new Asn1Tag((TagClass)tagClass, tagValue, false)); + Assert.False(reader.HasData); + + Assert.Equal(val1, val2); + } + + [Theory] + [InlineData("Empty", PublicEncodingRules.DER, "")] + [InlineData("Empty", PublicEncodingRules.CER, "")] + [InlineData("Empty", PublicEncodingRules.BER, "")] + [InlineData("TagOnly", PublicEncodingRules.BER, "01")] + [InlineData("TagOnly", PublicEncodingRules.CER, "01")] + [InlineData("TagOnly", PublicEncodingRules.DER, "01")] + [InlineData("MultiByte TagOnly", PublicEncodingRules.DER, "9F1F")] + [InlineData("MultiByte TagOnly", PublicEncodingRules.CER, "9F1F")] + [InlineData("MultiByte TagOnly", PublicEncodingRules.BER, "9F1F")] + [InlineData("TagAndLength", PublicEncodingRules.BER, "0101")] + [InlineData("Tag and MultiByteLength", PublicEncodingRules.BER, "01820001")] + [InlineData("TagAndLength", PublicEncodingRules.CER, "8001")] + [InlineData("TagAndLength", PublicEncodingRules.DER, "C001")] + [InlineData("MultiByteTagAndLength", PublicEncodingRules.DER, "9F2001")] + [InlineData("MultiByteTagAndLength", PublicEncodingRules.CER, "9F2001")] + [InlineData("MultiByteTagAndLength", PublicEncodingRules.BER, "9F2001")] + [InlineData("MultiByteTagAndMultiByteLength", PublicEncodingRules.BER, "9F28200001")] + [InlineData("TooShort", PublicEncodingRules.BER, "0100")] + [InlineData("TooShort", PublicEncodingRules.CER, "8000")] + [InlineData("TooShort", PublicEncodingRules.DER, "0100")] + [InlineData("TooLong", PublicEncodingRules.DER, "C0020000")] + [InlineData("TooLong", PublicEncodingRules.CER, "01020000")] + [InlineData("TooLong", PublicEncodingRules.BER, "C081020000")] + [InlineData("MissingContents", PublicEncodingRules.BER, "C001")] + [InlineData("MissingContents", PublicEncodingRules.CER, "0101")] + [InlineData("MissingContents", PublicEncodingRules.DER, "8001")] + [InlineData("NonCanonical", PublicEncodingRules.DER, "0101FE")] + [InlineData("NonCanonical", PublicEncodingRules.CER, "800101")] + [InlineData("Constructed", PublicEncodingRules.BER, "2103010101")] + [InlineData("Constructed", PublicEncodingRules.CER, "2103010101")] + [InlineData("Constructed", PublicEncodingRules.DER, "2103010101")] + [InlineData("WrongTag", PublicEncodingRules.DER, "0400")] + [InlineData("WrongTag", PublicEncodingRules.CER, "0400")] + [InlineData("WrongTag", PublicEncodingRules.BER, "0400")] + public static void ReadBoolean_Failure( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + Asn1Tag tag = default(Asn1Tag); + + if (inputData.Length > 0) + { + tag = reader.PeekTag(); + } + + if (tag.TagClass == TagClass.Universal) + { + Assert.Throws<CryptographicException>(() => reader.ReadBoolean()); + } + else + { + Assert.Throws<CryptographicException>(() => reader.ReadBoolean(tag)); + } + + if (inputData.Length == 0) + { + // If we started with nothing, where did the data come from? + Assert.False(reader.HasData, "reader.HasData"); + } + else + { + // Nothing should have moved + Assert.True(reader.HasData, "reader.HasData"); + } + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadEnumerated.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadEnumerated.cs new file mode 100644 index 0000000000..b921496d79 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadEnumerated.cs @@ -0,0 +1,760 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Reflection; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadEnumerated : Asn1ReaderTests + { + public enum ByteBacked : byte + { + Zero = 0, + NotFluffy = 11, + Fluff = 12, + } + + public enum SByteBacked : sbyte + { + Zero = 0, + Fluff = 83, + Pillow = -17, + } + + public enum ShortBacked : short + { + Zero = 0, + Fluff = 521, + Pillow = -1024, + } + + public enum UShortBacked : ushort + { + Zero = 0, + Fluff = 32768, + } + + public enum IntBacked : int + { + Zero = 0, + Fluff = 0x010001, + Pillow = -Fluff, + } + + public enum UIntBacked : uint + { + Zero = 0, + Fluff = 0x80000005, + } + + public enum LongBacked : long + { + Zero = 0, + Fluff = 0x0200000441, + Pillow = -0x100000000L, + } + + public enum ULongBacked : ulong + { + Zero = 0, + Fluff = 0xFACEF00DCAFEBEEF, + } + + private static void GetExpectedValue<TEnum>( + PublicEncodingRules ruleSet, + TEnum expectedValue, + string inputHex) + where TEnum : struct + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + TEnum value = reader.GetEnumeratedValue<TEnum>(); + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, ByteBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.CER, ByteBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.DER, ByteBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.BER, ByteBacked.Fluff, "0A010C")] + [InlineData(PublicEncodingRules.CER, ByteBacked.Fluff, "0A010C")] + [InlineData(PublicEncodingRules.DER, ByteBacked.Fluff, "0A010C")] + [InlineData(PublicEncodingRules.BER, (ByteBacked)255, "0A0200FF")] + [InlineData(PublicEncodingRules.CER, (ByteBacked)128, "0A020080")] + [InlineData(PublicEncodingRules.DER, (ByteBacked)129, "0A020081")] + [InlineData(PublicEncodingRules.BER, (ByteBacked)254, "0A82000200FE")] + public static void GetExpectedValue_ByteBacked( + PublicEncodingRules ruleSet, + ByteBacked expectedValue, + string inputHex) + { + GetExpectedValue(ruleSet, expectedValue, inputHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, SByteBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.CER, SByteBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.DER, SByteBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.BER, SByteBacked.Fluff, "0A0153")] + [InlineData(PublicEncodingRules.CER, SByteBacked.Fluff, "0A0153")] + [InlineData(PublicEncodingRules.DER, SByteBacked.Fluff, "0A0153")] + [InlineData(PublicEncodingRules.BER, SByteBacked.Pillow, "0A01EF")] + [InlineData(PublicEncodingRules.CER, (SByteBacked)sbyte.MinValue, "0A0180")] + [InlineData(PublicEncodingRules.DER, (SByteBacked)sbyte.MinValue + 1, "0A0181")] + [InlineData(PublicEncodingRules.BER, SByteBacked.Pillow, "0A820001EF")] + public static void GetExpectedValue_SByteBacked( + PublicEncodingRules ruleSet, + SByteBacked expectedValue, + string inputHex) + { + GetExpectedValue(ruleSet, expectedValue, inputHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, ShortBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.CER, ShortBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.DER, ShortBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.BER, ShortBacked.Fluff, "0A020209")] + [InlineData(PublicEncodingRules.CER, ShortBacked.Fluff, "0A020209")] + [InlineData(PublicEncodingRules.DER, ShortBacked.Fluff, "0A020209")] + [InlineData(PublicEncodingRules.BER, ShortBacked.Pillow, "0A02FC00")] + [InlineData(PublicEncodingRules.CER, (ShortBacked)short.MinValue, "0A028000")] + [InlineData(PublicEncodingRules.DER, (ShortBacked)short.MinValue + 1, "0A028001")] + [InlineData(PublicEncodingRules.BER, ShortBacked.Pillow, "0A820002FC00")] + public static void GetExpectedValue_ShortBacked( + PublicEncodingRules ruleSet, + ShortBacked expectedValue, + string inputHex) + { + GetExpectedValue(ruleSet, expectedValue, inputHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, UShortBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.CER, UShortBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.DER, UShortBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.BER, UShortBacked.Fluff, "0A03008000")] + [InlineData(PublicEncodingRules.CER, UShortBacked.Fluff, "0A03008000")] + [InlineData(PublicEncodingRules.DER, UShortBacked.Fluff, "0A03008000")] + [InlineData(PublicEncodingRules.BER, (UShortBacked)255, "0A0200FF")] + [InlineData(PublicEncodingRules.CER, (UShortBacked)256, "0A020100")] + [InlineData(PublicEncodingRules.DER, (UShortBacked)0x7FED, "0A027FED")] + [InlineData(PublicEncodingRules.BER, (UShortBacked)ushort.MaxValue, "0A82000300FFFF")] + [InlineData(PublicEncodingRules.BER, (UShortBacked)0x8123, "0A820003008123")] + public static void GetExpectedValue_UShortBacked( + PublicEncodingRules ruleSet, + UShortBacked expectedValue, + string inputHex) + { + GetExpectedValue(ruleSet, expectedValue, inputHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, IntBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.CER, IntBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.DER, IntBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.BER, IntBacked.Fluff, "0A03010001")] + [InlineData(PublicEncodingRules.CER, IntBacked.Fluff, "0A03010001")] + [InlineData(PublicEncodingRules.DER, IntBacked.Fluff, "0A03010001")] + [InlineData(PublicEncodingRules.BER, IntBacked.Pillow, "0A03FEFFFF")] + [InlineData(PublicEncodingRules.CER, (IntBacked)int.MinValue, "0A0480000000")] + [InlineData(PublicEncodingRules.DER, (IntBacked)int.MinValue + 1, "0A0480000001")] + [InlineData(PublicEncodingRules.BER, IntBacked.Pillow, "0A820003FEFFFF")] + public static void GetExpectedValue_IntBacked( + PublicEncodingRules ruleSet, + IntBacked expectedValue, + string inputHex) + { + GetExpectedValue(ruleSet, expectedValue, inputHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, UIntBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.CER, UIntBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.DER, UIntBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.BER, UIntBacked.Fluff, "0A050080000005")] + [InlineData(PublicEncodingRules.CER, UIntBacked.Fluff, "0A050080000005")] + [InlineData(PublicEncodingRules.DER, UIntBacked.Fluff, "0A050080000005")] + [InlineData(PublicEncodingRules.BER, (UIntBacked)255, "0A0200FF")] + [InlineData(PublicEncodingRules.CER, (UIntBacked)256, "0A020100")] + [InlineData(PublicEncodingRules.DER, (UIntBacked)0x7FED, "0A027FED")] + [InlineData(PublicEncodingRules.BER, (UIntBacked)uint.MaxValue, "0A82000500FFFFFFFF")] + [InlineData(PublicEncodingRules.BER, (UIntBacked)0x8123, "0A820003008123")] + public static void GetExpectedValue_UIntBacked( + PublicEncodingRules ruleSet, + UIntBacked expectedValue, + string inputHex) + { + GetExpectedValue(ruleSet, expectedValue, inputHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, LongBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.CER, LongBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.DER, LongBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.BER, LongBacked.Fluff, "0A050200000441")] + [InlineData(PublicEncodingRules.CER, LongBacked.Fluff, "0A050200000441")] + [InlineData(PublicEncodingRules.DER, LongBacked.Fluff, "0A050200000441")] + [InlineData(PublicEncodingRules.BER, LongBacked.Pillow, "0A05FF00000000")] + [InlineData(PublicEncodingRules.CER, (LongBacked)short.MinValue, "0A028000")] + [InlineData(PublicEncodingRules.DER, (LongBacked)short.MinValue + 1, "0A028001")] + [InlineData(PublicEncodingRules.BER, LongBacked.Pillow, "0A820005FF00000000")] + public static void GetExpectedValue_LongBacked( + PublicEncodingRules ruleSet, + LongBacked expectedValue, + string inputHex) + { + GetExpectedValue(ruleSet, expectedValue, inputHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, ULongBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.CER, ULongBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.DER, ULongBacked.Zero, "0A0100")] + [InlineData(PublicEncodingRules.BER, ULongBacked.Fluff, "0A0900FACEF00DCAFEBEEF")] + [InlineData(PublicEncodingRules.CER, ULongBacked.Fluff, "0A0900FACEF00DCAFEBEEF")] + [InlineData(PublicEncodingRules.DER, ULongBacked.Fluff, "0A0900FACEF00DCAFEBEEF")] + [InlineData(PublicEncodingRules.BER, (ULongBacked)255, "0A0200FF")] + [InlineData(PublicEncodingRules.CER, (ULongBacked)256, "0A020100")] + [InlineData(PublicEncodingRules.DER, (ULongBacked)0x7FED, "0A027FED")] + [InlineData(PublicEncodingRules.BER, (ULongBacked)uint.MaxValue, "0A82000500FFFFFFFF")] + [InlineData(PublicEncodingRules.BER, (ULongBacked)ulong.MaxValue, "0A82000900FFFFFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.BER, (ULongBacked)0x8123, "0A820003008123")] + public static void GetExpectedValue_ULongBacked( + PublicEncodingRules ruleSet, + ULongBacked expectedValue, + string inputHex) + { + GetExpectedValue(ruleSet, expectedValue, inputHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "")] + [InlineData(PublicEncodingRules.CER, "")] + [InlineData(PublicEncodingRules.DER, "")] + [InlineData(PublicEncodingRules.BER, "0A")] + [InlineData(PublicEncodingRules.CER, "0A")] + [InlineData(PublicEncodingRules.DER, "0A")] + [InlineData(PublicEncodingRules.BER, "0A00")] + [InlineData(PublicEncodingRules.CER, "0A00")] + [InlineData(PublicEncodingRules.DER, "0A00")] + [InlineData(PublicEncodingRules.BER, "0A01")] + [InlineData(PublicEncodingRules.CER, "0A01")] + [InlineData(PublicEncodingRules.DER, "0A01")] + [InlineData(PublicEncodingRules.BER, "0A81")] + [InlineData(PublicEncodingRules.CER, "0A81")] + [InlineData(PublicEncodingRules.DER, "0A81")] + [InlineData(PublicEncodingRules.BER, "9F00")] + [InlineData(PublicEncodingRules.CER, "9F00")] + [InlineData(PublicEncodingRules.DER, "9F00")] + [InlineData(PublicEncodingRules.BER, "0A01FF")] + [InlineData(PublicEncodingRules.CER, "0A01FF")] + [InlineData(PublicEncodingRules.DER, "0A01FF")] + [InlineData(PublicEncodingRules.BER, "0A02007F")] + [InlineData(PublicEncodingRules.CER, "0A02007F")] + [InlineData(PublicEncodingRules.DER, "0A02007F")] + [InlineData(PublicEncodingRules.BER, "0A020102")] + [InlineData(PublicEncodingRules.CER, "0A020102")] + [InlineData(PublicEncodingRules.DER, "0A020102")] + [InlineData(PublicEncodingRules.BER, "0A02FF80")] + [InlineData(PublicEncodingRules.CER, "0A02FF80")] + [InlineData(PublicEncodingRules.DER, "0A02FF80")] + [InlineData(PublicEncodingRules.BER, "0A03010203")] + [InlineData(PublicEncodingRules.CER, "0A03010203")] + [InlineData(PublicEncodingRules.DER, "0A03010203")] + [InlineData(PublicEncodingRules.BER, "0A0401020304")] + [InlineData(PublicEncodingRules.CER, "0A0401020304")] + [InlineData(PublicEncodingRules.DER, "0A0401020304")] + [InlineData(PublicEncodingRules.BER, "0A050102030405")] + [InlineData(PublicEncodingRules.CER, "0A050102030405")] + [InlineData(PublicEncodingRules.DER, "0A050102030405")] + [InlineData(PublicEncodingRules.BER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.CER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.DER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.BER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.CER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.DER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.BER, "2A030A0100")] + public static void GetEnumeratedValue_Invalid_Byte(PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.GetEnumeratedValue<ByteBacked>()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "")] + [InlineData(PublicEncodingRules.CER, "")] + [InlineData(PublicEncodingRules.DER, "")] + [InlineData(PublicEncodingRules.BER, "0A")] + [InlineData(PublicEncodingRules.CER, "0A")] + [InlineData(PublicEncodingRules.DER, "0A")] + [InlineData(PublicEncodingRules.BER, "0A00")] + [InlineData(PublicEncodingRules.CER, "0A00")] + [InlineData(PublicEncodingRules.DER, "0A00")] + [InlineData(PublicEncodingRules.BER, "0A01")] + [InlineData(PublicEncodingRules.CER, "0A01")] + [InlineData(PublicEncodingRules.DER, "0A01")] + [InlineData(PublicEncodingRules.BER, "0A81")] + [InlineData(PublicEncodingRules.CER, "0A81")] + [InlineData(PublicEncodingRules.DER, "0A81")] + [InlineData(PublicEncodingRules.BER, "9F00")] + [InlineData(PublicEncodingRules.CER, "9F00")] + [InlineData(PublicEncodingRules.DER, "9F00")] + [InlineData(PublicEncodingRules.BER, "0A02007F")] + [InlineData(PublicEncodingRules.CER, "0A02007F")] + [InlineData(PublicEncodingRules.DER, "0A02007F")] + [InlineData(PublicEncodingRules.BER, "0A020102")] + [InlineData(PublicEncodingRules.CER, "0A020102")] + [InlineData(PublicEncodingRules.DER, "0A020102")] + [InlineData(PublicEncodingRules.BER, "0A02FF80")] + [InlineData(PublicEncodingRules.CER, "0A02FF80")] + [InlineData(PublicEncodingRules.DER, "0A02FF80")] + [InlineData(PublicEncodingRules.BER, "0A03010203")] + [InlineData(PublicEncodingRules.CER, "0A03010203")] + [InlineData(PublicEncodingRules.DER, "0A03010203")] + [InlineData(PublicEncodingRules.BER, "0A0401020304")] + [InlineData(PublicEncodingRules.CER, "0A0401020304")] + [InlineData(PublicEncodingRules.DER, "0A0401020304")] + [InlineData(PublicEncodingRules.BER, "0A050102030405")] + [InlineData(PublicEncodingRules.CER, "0A050102030405")] + [InlineData(PublicEncodingRules.DER, "0A050102030405")] + [InlineData(PublicEncodingRules.BER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.CER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.DER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.BER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.CER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.DER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.BER, "2A030A0100")] + public static void GetEnumeratedValue_Invalid_SByte(PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.GetEnumeratedValue<SByteBacked>()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "")] + [InlineData(PublicEncodingRules.CER, "")] + [InlineData(PublicEncodingRules.DER, "")] + [InlineData(PublicEncodingRules.BER, "0A")] + [InlineData(PublicEncodingRules.CER, "0A")] + [InlineData(PublicEncodingRules.DER, "0A")] + [InlineData(PublicEncodingRules.BER, "0A00")] + [InlineData(PublicEncodingRules.CER, "0A00")] + [InlineData(PublicEncodingRules.DER, "0A00")] + [InlineData(PublicEncodingRules.BER, "0A01")] + [InlineData(PublicEncodingRules.CER, "0A01")] + [InlineData(PublicEncodingRules.DER, "0A01")] + [InlineData(PublicEncodingRules.BER, "0A81")] + [InlineData(PublicEncodingRules.CER, "0A81")] + [InlineData(PublicEncodingRules.DER, "0A81")] + [InlineData(PublicEncodingRules.BER, "9F00")] + [InlineData(PublicEncodingRules.CER, "9F00")] + [InlineData(PublicEncodingRules.DER, "9F00")] + [InlineData(PublicEncodingRules.BER, "0A02007F")] + [InlineData(PublicEncodingRules.CER, "0A02007F")] + [InlineData(PublicEncodingRules.DER, "0A02007F")] + [InlineData(PublicEncodingRules.BER, "0A02FF80")] + [InlineData(PublicEncodingRules.CER, "0A02FF80")] + [InlineData(PublicEncodingRules.DER, "0A02FF80")] + [InlineData(PublicEncodingRules.BER, "0A03010203")] + [InlineData(PublicEncodingRules.CER, "0A03010203")] + [InlineData(PublicEncodingRules.DER, "0A03010203")] + [InlineData(PublicEncodingRules.BER, "0A0401020304")] + [InlineData(PublicEncodingRules.CER, "0A0401020304")] + [InlineData(PublicEncodingRules.DER, "0A0401020304")] + [InlineData(PublicEncodingRules.BER, "0A050102030405")] + [InlineData(PublicEncodingRules.CER, "0A050102030405")] + [InlineData(PublicEncodingRules.DER, "0A050102030405")] + [InlineData(PublicEncodingRules.BER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.CER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.DER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.BER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.CER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.DER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.BER, "2A030A0100")] + public static void GetEnumeratedValue_Invalid_Short(PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.GetEnumeratedValue<ShortBacked>()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "")] + [InlineData(PublicEncodingRules.CER, "")] + [InlineData(PublicEncodingRules.DER, "")] + [InlineData(PublicEncodingRules.BER, "0A")] + [InlineData(PublicEncodingRules.CER, "0A")] + [InlineData(PublicEncodingRules.DER, "0A")] + [InlineData(PublicEncodingRules.BER, "0A00")] + [InlineData(PublicEncodingRules.CER, "0A00")] + [InlineData(PublicEncodingRules.DER, "0A00")] + [InlineData(PublicEncodingRules.BER, "0A01")] + [InlineData(PublicEncodingRules.CER, "0A01")] + [InlineData(PublicEncodingRules.DER, "0A01")] + [InlineData(PublicEncodingRules.BER, "0A81")] + [InlineData(PublicEncodingRules.CER, "0A81")] + [InlineData(PublicEncodingRules.DER, "0A81")] + [InlineData(PublicEncodingRules.BER, "9F00")] + [InlineData(PublicEncodingRules.CER, "9F00")] + [InlineData(PublicEncodingRules.DER, "9F00")] + [InlineData(PublicEncodingRules.BER, "0A01FF")] + [InlineData(PublicEncodingRules.CER, "0A01FF")] + [InlineData(PublicEncodingRules.DER, "0A01FF")] + [InlineData(PublicEncodingRules.BER, "0A02007F")] + [InlineData(PublicEncodingRules.CER, "0A02007F")] + [InlineData(PublicEncodingRules.DER, "0A02007F")] + [InlineData(PublicEncodingRules.BER, "0A02FF80")] + [InlineData(PublicEncodingRules.CER, "0A02FF80")] + [InlineData(PublicEncodingRules.DER, "0A02FF80")] + [InlineData(PublicEncodingRules.BER, "0A03010203")] + [InlineData(PublicEncodingRules.CER, "0A03010203")] + [InlineData(PublicEncodingRules.DER, "0A03010203")] + [InlineData(PublicEncodingRules.BER, "0A0401020304")] + [InlineData(PublicEncodingRules.CER, "0A0401020304")] + [InlineData(PublicEncodingRules.DER, "0A0401020304")] + [InlineData(PublicEncodingRules.BER, "0A050102030405")] + [InlineData(PublicEncodingRules.CER, "0A050102030405")] + [InlineData(PublicEncodingRules.DER, "0A050102030405")] + [InlineData(PublicEncodingRules.BER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.CER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.DER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.BER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.CER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.DER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.BER, "2A030A0100")] + public static void GetEnumeratedValue_Invalid_UShort(PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.GetEnumeratedValue<UShortBacked>()); + } + + + [Theory] + [InlineData(PublicEncodingRules.BER, "")] + [InlineData(PublicEncodingRules.CER, "")] + [InlineData(PublicEncodingRules.DER, "")] + [InlineData(PublicEncodingRules.BER, "0A")] + [InlineData(PublicEncodingRules.CER, "0A")] + [InlineData(PublicEncodingRules.DER, "0A")] + [InlineData(PublicEncodingRules.BER, "0A00")] + [InlineData(PublicEncodingRules.CER, "0A00")] + [InlineData(PublicEncodingRules.DER, "0A00")] + [InlineData(PublicEncodingRules.BER, "0A01")] + [InlineData(PublicEncodingRules.CER, "0A01")] + [InlineData(PublicEncodingRules.DER, "0A01")] + [InlineData(PublicEncodingRules.BER, "0A81")] + [InlineData(PublicEncodingRules.CER, "0A81")] + [InlineData(PublicEncodingRules.DER, "0A81")] + [InlineData(PublicEncodingRules.BER, "9F00")] + [InlineData(PublicEncodingRules.CER, "9F00")] + [InlineData(PublicEncodingRules.DER, "9F00")] + [InlineData(PublicEncodingRules.BER, "0A02007F")] + [InlineData(PublicEncodingRules.CER, "0A02007F")] + [InlineData(PublicEncodingRules.DER, "0A02007F")] + [InlineData(PublicEncodingRules.BER, "0A02FF80")] + [InlineData(PublicEncodingRules.CER, "0A02FF80")] + [InlineData(PublicEncodingRules.DER, "0A02FF80")] + [InlineData(PublicEncodingRules.BER, "0A050102030405")] + [InlineData(PublicEncodingRules.CER, "0A050102030405")] + [InlineData(PublicEncodingRules.DER, "0A050102030405")] + [InlineData(PublicEncodingRules.BER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.CER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.DER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.BER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.CER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.DER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.BER, "2A030A0100")] + public static void GetEnumeratedValue_Invalid_Int(PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.GetEnumeratedValue<IntBacked>()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "")] + [InlineData(PublicEncodingRules.CER, "")] + [InlineData(PublicEncodingRules.DER, "")] + [InlineData(PublicEncodingRules.BER, "0A")] + [InlineData(PublicEncodingRules.CER, "0A")] + [InlineData(PublicEncodingRules.DER, "0A")] + [InlineData(PublicEncodingRules.BER, "0A00")] + [InlineData(PublicEncodingRules.CER, "0A00")] + [InlineData(PublicEncodingRules.DER, "0A00")] + [InlineData(PublicEncodingRules.BER, "0A01")] + [InlineData(PublicEncodingRules.CER, "0A01")] + [InlineData(PublicEncodingRules.DER, "0A01")] + [InlineData(PublicEncodingRules.BER, "0A81")] + [InlineData(PublicEncodingRules.CER, "0A81")] + [InlineData(PublicEncodingRules.DER, "0A81")] + [InlineData(PublicEncodingRules.BER, "9F00")] + [InlineData(PublicEncodingRules.CER, "9F00")] + [InlineData(PublicEncodingRules.DER, "9F00")] + [InlineData(PublicEncodingRules.BER, "0A01FF")] + [InlineData(PublicEncodingRules.CER, "0A01FF")] + [InlineData(PublicEncodingRules.DER, "0A01FF")] + [InlineData(PublicEncodingRules.BER, "0A02007F")] + [InlineData(PublicEncodingRules.CER, "0A02007F")] + [InlineData(PublicEncodingRules.DER, "0A02007F")] + [InlineData(PublicEncodingRules.BER, "0A050102030405")] + [InlineData(PublicEncodingRules.CER, "0A050102030405")] + [InlineData(PublicEncodingRules.DER, "0A050102030405")] + [InlineData(PublicEncodingRules.BER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.CER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.DER, "0A080102030405060708")] + [InlineData(PublicEncodingRules.BER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.CER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.DER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.BER, "2A030A0100")] + public static void GetEnumeratedValue_Invalid_UInt(PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.GetEnumeratedValue<UIntBacked>()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "")] + [InlineData(PublicEncodingRules.CER, "")] + [InlineData(PublicEncodingRules.DER, "")] + [InlineData(PublicEncodingRules.BER, "0A")] + [InlineData(PublicEncodingRules.CER, "0A")] + [InlineData(PublicEncodingRules.DER, "0A")] + [InlineData(PublicEncodingRules.BER, "0A00")] + [InlineData(PublicEncodingRules.CER, "0A00")] + [InlineData(PublicEncodingRules.DER, "0A00")] + [InlineData(PublicEncodingRules.BER, "0A01")] + [InlineData(PublicEncodingRules.CER, "0A01")] + [InlineData(PublicEncodingRules.DER, "0A01")] + [InlineData(PublicEncodingRules.BER, "0A81")] + [InlineData(PublicEncodingRules.CER, "0A81")] + [InlineData(PublicEncodingRules.DER, "0A81")] + [InlineData(PublicEncodingRules.BER, "9F00")] + [InlineData(PublicEncodingRules.CER, "9F00")] + [InlineData(PublicEncodingRules.DER, "9F00")] + [InlineData(PublicEncodingRules.BER, "0A02007F")] + [InlineData(PublicEncodingRules.CER, "0A02007F")] + [InlineData(PublicEncodingRules.DER, "0A02007F")] + [InlineData(PublicEncodingRules.BER, "0A02FF80")] + [InlineData(PublicEncodingRules.CER, "0A02FF80")] + [InlineData(PublicEncodingRules.DER, "0A02FF80")] + [InlineData(PublicEncodingRules.BER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.CER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.DER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.BER, "2A030A0100")] + public static void GetEnumeratedValue_Invalid_Long(PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.GetEnumeratedValue<LongBacked>()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "")] + [InlineData(PublicEncodingRules.CER, "")] + [InlineData(PublicEncodingRules.DER, "")] + [InlineData(PublicEncodingRules.BER, "0A")] + [InlineData(PublicEncodingRules.CER, "0A")] + [InlineData(PublicEncodingRules.DER, "0A")] + [InlineData(PublicEncodingRules.BER, "0A00")] + [InlineData(PublicEncodingRules.CER, "0A00")] + [InlineData(PublicEncodingRules.DER, "0A00")] + [InlineData(PublicEncodingRules.BER, "0A01")] + [InlineData(PublicEncodingRules.CER, "0A01")] + [InlineData(PublicEncodingRules.DER, "0A01")] + [InlineData(PublicEncodingRules.BER, "0A81")] + [InlineData(PublicEncodingRules.CER, "0A81")] + [InlineData(PublicEncodingRules.DER, "0A81")] + [InlineData(PublicEncodingRules.BER, "9F00")] + [InlineData(PublicEncodingRules.CER, "9F00")] + [InlineData(PublicEncodingRules.DER, "9F00")] + [InlineData(PublicEncodingRules.BER, "0A01FF")] + [InlineData(PublicEncodingRules.CER, "0A01FF")] + [InlineData(PublicEncodingRules.DER, "0A01FF")] + [InlineData(PublicEncodingRules.BER, "0A02007F")] + [InlineData(PublicEncodingRules.CER, "0A02007F")] + [InlineData(PublicEncodingRules.DER, "0A02007F")] + [InlineData(PublicEncodingRules.BER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.CER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.DER, "0A09010203040506070809")] + [InlineData(PublicEncodingRules.BER, "2A030A0100")] + public static void GetEnumeratedValue_Invalid_ULong(PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.GetEnumeratedValue<ULongBacked>()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void GetEnumeratedValue_NonEnumType(PublicEncodingRules ruleSet) + { + byte[] data = { 0x0A, 0x01, 0x00 }; + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + Assert.Throws<ArgumentException>(() => reader.GetEnumeratedValue<Guid>()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void GetEnumeratedValue_FlagsEnum(PublicEncodingRules ruleSet) + { + byte[] data = { 0x0A, 0x01, 0x00 }; + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tEnum", + () => reader.GetEnumeratedValue<AssemblyFlags>()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void GetEnumeratedBytes(PublicEncodingRules ruleSet) + { + const string Payload = "0102030405060708090A0B0C0D0E0F10"; + + // ENUMERATED (payload) followed by INTEGER (0) + byte[] data = ("0A10" + Payload + "020100").HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + ReadOnlyMemory<byte> contents = reader.GetEnumeratedBytes(); + Assert.Equal(0x10, contents.Length); + Assert.Equal(Payload, contents.ByteArrayToHex()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "")] + [InlineData(PublicEncodingRules.CER, "")] + [InlineData(PublicEncodingRules.DER, "")] + [InlineData(PublicEncodingRules.BER, "0A")] + [InlineData(PublicEncodingRules.CER, "0A")] + [InlineData(PublicEncodingRules.DER, "0A")] + [InlineData(PublicEncodingRules.BER, "0A00")] + [InlineData(PublicEncodingRules.CER, "0A00")] + [InlineData(PublicEncodingRules.DER, "0A00")] + [InlineData(PublicEncodingRules.BER, "0A01")] + [InlineData(PublicEncodingRules.CER, "0A01")] + [InlineData(PublicEncodingRules.DER, "0A01")] + [InlineData(PublicEncodingRules.BER, "010100")] + [InlineData(PublicEncodingRules.CER, "010100")] + [InlineData(PublicEncodingRules.DER, "010100")] + [InlineData(PublicEncodingRules.BER, "9F00")] + [InlineData(PublicEncodingRules.CER, "9F00")] + [InlineData(PublicEncodingRules.DER, "9F00")] + [InlineData(PublicEncodingRules.BER, "0A81")] + [InlineData(PublicEncodingRules.CER, "0A81")] + [InlineData(PublicEncodingRules.DER, "0A81")] + public static void GetEnumeratedBytes_Throws(PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.GetEnumeratedBytes()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x0A, 1, 0x7E }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.GetEnumeratedValue<ShortBacked>(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.GetEnumeratedValue<ShortBacked>(new Asn1Tag(TagClass.ContextSpecific, 0))); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + ShortBacked value = reader.GetEnumeratedValue<ShortBacked>(); + Assert.Equal((ShortBacked)0x7E, value); + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x87, 2, 0, 0x80 }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.GetEnumeratedValue<ShortBacked>(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.GetEnumeratedValue<ShortBacked>()); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.GetEnumeratedValue<ShortBacked>(new Asn1Tag(TagClass.Application, 0))); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.GetEnumeratedValue<ShortBacked>(new Asn1Tag(TagClass.ContextSpecific, 1))); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + ShortBacked value = reader.GetEnumeratedValue<ShortBacked>(new Asn1Tag(TagClass.ContextSpecific, 7)); + Assert.Equal((ShortBacked)0x80, value); + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "0A01FF", PublicTagClass.Universal, 10)] + [InlineData(PublicEncodingRules.CER, "0A01FF", PublicTagClass.Universal, 10)] + [InlineData(PublicEncodingRules.DER, "0A01FF", PublicTagClass.Universal, 10)] + [InlineData(PublicEncodingRules.BER, "8001FF", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C01FF", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A4601FF", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + ShortBacked val1 = reader.GetEnumeratedValue<ShortBacked>(new Asn1Tag((TagClass)tagClass, tagValue, true)); + Assert.False(reader.HasData); + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + ShortBacked val2 = reader.GetEnumeratedValue<ShortBacked>(new Asn1Tag((TagClass)tagClass, tagValue, false)); + Assert.False(reader.HasData); + + Assert.Equal(val1, val2); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadGeneralizedTime.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadGeneralizedTime.cs new file mode 100644 index 0000000000..02dfa3810d --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadGeneralizedTime.cs @@ -0,0 +1,522 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadGeneralizedTime : Asn1ReaderTests + { + [Theory] + // yyyyMMddHH (2017090821) + [InlineData(PublicEncodingRules.BER, "180A32303137303930383231", 2017, 9, 8, 21, 0, 0, 0, null, 0)] + // yyyyMMddHHZ (2017090821Z) + [InlineData(PublicEncodingRules.BER, "180B323031373039303832315A", 2017, 9, 8, 21, 0, 0, 0, 0, 0)] + // yyyyMMddHH-HH (2017090821-01) + [InlineData(PublicEncodingRules.BER, "180D323031373039303832312D3031", 2017, 9, 8, 21, 0, 0, 0, -1, 0)] + // yyyyMMddHH+HHmm (2017090821+0118) + [InlineData(PublicEncodingRules.BER, "180F323031373039303832312B30313138", 2017, 9, 8, 21, 0, 0, 0, 1, 18)] + // yyyyMMddHH,hourFrac (2017090821.1) + [InlineData(PublicEncodingRules.BER, "180C323031373039303832312C31", 2017, 9, 8, 21, 6, 0, 0, null, 0)] + // yyyyMMddHH.hourFracZ (2017090821.2010Z) + [InlineData(PublicEncodingRules.BER, "1810323031373039303832312E323031305A", 2017, 9, 8, 21, 12, 3, 600, 0, 0)] + // yyyyMMddHH,hourFrac-HH (2017090821,3099-01) + [InlineData(PublicEncodingRules.BER, "1812323031373039303832312C333039392D3031", 2017, 9, 8, 21, 18, 35, 640, -1, 0)] + // yyyyMMddHH.hourFrac+HHmm (2017090821.201+0118) + [InlineData(PublicEncodingRules.BER, "1813323031373039303832312E3230312B30313138", 2017, 9, 8, 21, 12, 3, 600, 1, 18)] + // yyyyMMddHHmm (201709082358) + [InlineData(PublicEncodingRules.BER, "180C323031373039303832333538", 2017, 9, 8, 23, 58, 0, 0, null, 0)] + // yyyyMMddHHmmZ (201709082358Z) + [InlineData(PublicEncodingRules.BER, "180D3230313730393038323335385A", 2017, 9, 8, 23, 58, 0, 0, 0, 0)] + // yyyyMMddHHmm-HH (201709082358-01) + [InlineData(PublicEncodingRules.BER, "180F3230313730393038323335382D3031", 2017, 9, 8, 23, 58, 0, 0, -1, 0)] + // yyyyMMddHHmm+HHmm (201709082358+0118) + [InlineData(PublicEncodingRules.BER, "18113230313730393038323335382B30313138", 2017, 9, 8, 23, 58, 0, 0, 1, 18)] + // yyyyMMddHHmm.minuteFrac (201709082358.01) + [InlineData(PublicEncodingRules.BER, "180F3230313730393038323335382E3031", 2017, 9, 8, 23, 58, 0, 600, null, 0)] + // yyyyMMddHHmm,minuteFracZ (201709082358,11Z) + [InlineData(PublicEncodingRules.BER, "18103230313730393038323335382C31315A", 2017, 9, 8, 23, 58, 6, 600, 0, 0)] + // yyyyMMddHHmm.minuteFrac-HH (201709082358.05-01) + [InlineData(PublicEncodingRules.BER, "18123230313730393038323335382E30352D3031", 2017, 9, 8, 23, 58, 3, 0, -1, 0)] + // yyyyMMddHHmm,minuteFrac+HHmm (201709082358,007+0118) + [InlineData(PublicEncodingRules.BER, "18153230313730393038323335382C3030372B30313138", 2017, 9, 8, 23, 58, 0, 420, 1, 18)] + // yyyyMMddHHmmss (20161106012345) - Ambiguous time due to DST "fall back" in US & Canada + [InlineData(PublicEncodingRules.BER, "180E3230313631313036303132333435", 2016, 11, 6, 1, 23, 45, 0, null, 0)] + // yyyyMMddHHmmssZ (20161106012345Z) + [InlineData(PublicEncodingRules.BER, "180F32303136313130363031323334355A", 2016, 11, 6, 1, 23, 45, 0, 0, 0)] + // yyyyMMddHHmmss-HH (20161106012345-01) + [InlineData(PublicEncodingRules.BER, "181132303136313130363031323334352D3031", 2016, 11, 6, 1, 23, 45, 0, -1, 0)] + // yyyyMMddHHmmss+HHmm (20161106012345+0118) + [InlineData(PublicEncodingRules.BER, "181332303136313130363031323334352B30313138", 2016, 11, 6, 1, 23, 45, 0, 1, 18)] + // yyyyMMddHHmmss.secondFrac (20161106012345.6789) - Ambiguous time due to DST "fall back" in US & Canada + [InlineData(PublicEncodingRules.BER, "181332303136313130363031323334352E36373839", 2016, 11, 6, 1, 23, 45, 679, null, 0)] + // yyyyMMddHHmmss,secondFracZ (20161106012345,7654Z) + [InlineData(PublicEncodingRules.BER, "181432303136313130363031323334352C373635345A", 2016, 11, 6, 1, 23, 45, 765, 0, 0)] + // yyyyMMddHHmmss.secondFrac-HH (20161106012345.001-01) + [InlineData(PublicEncodingRules.BER, "181532303136313130363031323334352E3030312D3031", 2016, 11, 6, 1, 23, 45, 1, -1, 0)] + // yyyyMMddHHmmss,secondFrac+HHmm (20161106012345,0009+0118) + [InlineData(PublicEncodingRules.BER, "181832303136313130363031323334352C303030392B30313138", 2016, 11, 6, 1, 23, 45, 1, 1, 18)] + + // yyyyMMddHHmmssZ (20161106012345Z) + [InlineData(PublicEncodingRules.CER, "180F32303136313130363031323334355A", 2016, 11, 6, 1, 23, 45, 0, 0, 0)] + [InlineData(PublicEncodingRules.DER, "180F32303136313130363031323334355A", 2016, 11, 6, 1, 23, 45, 0, 0, 0)] + + // yyyyMMddHHmmss.secondFracZ (20161106012345,7654Z) + [InlineData(PublicEncodingRules.CER, "181432303136313130363031323334352E373635345A", 2016, 11, 6, 1, 23, 45, 765, 0, 0)] + [InlineData(PublicEncodingRules.DER, "181432303136313130363031323334352E373635345A", 2016, 11, 6, 1, 23, 45, 765, 0, 0)] + public static void ParseTime_Valid( + PublicEncodingRules ruleSet, + string inputHex, + int year, + int month, + int day, + int hour, + int minute, + int second, + int millisecond, + int? offsetHour, + int offsetMinute) + { + byte[] inputData = inputHex.HexToByteArray(); + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + DateTimeOffset value = reader.GetGeneralizedTime(); + Assert.False(reader.HasData, "reader.HasData"); + + Assert.Equal(year, value.Year); + Assert.Equal(month, value.Month); + Assert.Equal(day, value.Day); + Assert.Equal(hour, value.Hour); + Assert.Equal(minute, value.Minute); + Assert.Equal(second, value.Second); + Assert.Equal(millisecond, value.Millisecond); + + TimeSpan timeOffset; + + if (offsetHour == null) + { + // Ask the system what offset it thinks was relevant for that time. + // Includes DST ambiguity. + timeOffset = new DateTimeOffset(value.LocalDateTime).Offset; + } + else + { + timeOffset = new TimeSpan(offsetHour.Value, offsetMinute, 0); + } + + Assert.Equal(timeOffset, value.Offset); + } + + [Theory] + // yyyyMMddHH (2017090821) + [InlineData("180A32303137303930383231")] + // yyyyMMddHHZ (2017090821Z) + [InlineData("180B323031373039303832315A")] + // yyyyMMddHH-HH (2017090821-01) + [InlineData("180D323031373039303832312D3031")] + // yyyyMMddHH+HHmm (2017090821+0118) + [InlineData("180F323031373039303832312B30313138")] + // yyyyMMddHH,hourFrac (2017090821,1) + [InlineData("180C323031373039303832312C31")] + // yyyyMMddHH.hourFrac (2017090821.1) + [InlineData("180C323031373039303832312E31")] + // yyyyMMddHH,hourFracZ (2017090821,2010Z) + [InlineData("1810323031373039303832312C323031305A")] + // yyyyMMddHH.hourFracZ (2017090821.2010Z) + [InlineData("1810323031373039303832312E323031305A")] + // yyyyMMddHH,hourFrac-HH (2017090821,3099-01) + [InlineData("1812323031373039303832312C333039392D3031")] + // yyyyMMddHH.hourFrac-HH (2017090821.3099-01) + [InlineData("1812323031373039303832312E333039392D3031")] + // yyyyMMddHH,hourFrac+HHmm (2017090821,201+0118) + [InlineData("1813323031373039303832312C3230312B30313138")] + // yyyyMMddHH.hourFrac+HHmm (2017090821.201+0118) + [InlineData("1813323031373039303832312E3230312B30313138")] + // yyyyMMddHHmm (201709082358) + [InlineData("180C323031373039303832333538")] + // yyyyMMddHHmmZ (201709082358Z) + [InlineData("180D3230313730393038323335385A")] + // yyyyMMddHHmm-HH (201709082358-01) + [InlineData("180F3230313730393038323335382D3031")] + // yyyyMMddHHmm+HHmm (201709082358+0118) + [InlineData("18113230313730393038323335382B30313138")] + // yyyyMMddHHmm,minuteFrac (201709082358,01) + [InlineData("180F3230313730393038323335382C3031")] + // yyyyMMddHHmm.minuteFrac (201709082358.01) + [InlineData("180F3230313730393038323335382E3031")] + // yyyyMMddHHmm,minuteFracZ (201709082358,11Z) + [InlineData("18103230313730393038323335382C31315A")] + // yyyyMMddHHmm.minuteFracZ (201709082358.11Z) + [InlineData("18103230313730393038323335382E31315A")] + // yyyyMMddHHmm,minuteFrac-HH (201709082358m05-01) + [InlineData("18123230313730393038323335382C30352D3031")] + // yyyyMMddHHmm.minuteFrac-HH (201709082358.05-01) + [InlineData("18123230313730393038323335382E30352D3031")] + // yyyyMMddHHmm,minuteFrac+HHmm (201709082358,007+0118) + [InlineData("18153230313730393038323335382C3030372B30313138")] + // yyyyMMddHHmm.minuteFrac+HHmm (201709082358.007+0118) + [InlineData("18153230313730393038323335382E3030372B30313138")] + // yyyyMMddHHmmss (20161106012345) + [InlineData("180E3230313631313036303132333435")] + // yyyyMMddHHmmss-HH (20161106012345-01) + [InlineData("181132303136313130363031323334352D3031")] + // yyyyMMddHHmmss+HHmm (20161106012345+0118) + [InlineData("181332303136313130363031323334352B30313138")] + // yyyyMMddHHmmss,secondFrac (20161106012345,6789) + [InlineData("181332303136313130363031323334352C36373839")] + // yyyyMMddHHmmss.secondFrac (20161106012345.6789) + [InlineData("181332303136313130363031323334352E36373839")] + // yyyyMMddHHmmss,secondFracZ (20161106012345,7654Z) + [InlineData("181432303136313130363031323334352C373635345A")] + // yyyyMMddHHmmss,secondFrac-HH (20161106012345,001-01) + [InlineData("181532303136313130363031323334352C3030312D3031")] + // yyyyMMddHHmmss.secondFrac-HH (20161106012345.001-01) + [InlineData("181532303136313130363031323334352E3030312D3031")] + // yyyyMMddHHmmss,secondFrac+HHmm (20161106012345,0009+0118) + [InlineData("181832303136313130363031323334352C303030392B30313138")] + // yyyyMMddHHmmss.secondFrac+HHmm (20161106012345.0009+0118) + [InlineData("181832303136313130363031323334352E303030392B30313138")] + // yyyyMMddHHmmss.secondFrac0Z (20161106012345.76540Z) + [InlineData("181532303136313130363031323334352E37363534305A")] + // Constructed encoding of yyyyMMddHHmmssZ + [InlineData( + "3880" + + "040432303136" + + "04023131" + + "0403303630" + + "040131" + + "0405323334355A" + + "0000")] + public static void ParseTime_BerOnly(string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader cerReader = new AsnReader(inputData, AsnEncodingRules.CER); + AsnReader derReader = new AsnReader(inputData, AsnEncodingRules.DER); + + Assert.Throws<CryptographicException>(() => cerReader.GetGeneralizedTime()); + Assert.Throws<CryptographicException>(() => derReader.GetGeneralizedTime()); + + // Prove it was not just corrupt input + AsnReader berReader = new AsnReader(inputData, AsnEncodingRules.BER); + berReader.GetGeneralizedTime(); + Assert.False(berReader.HasData, "berReader.HasData"); + Assert.True(cerReader.HasData, "cerReader.HasData"); + Assert.True(derReader.HasData, "derReader.HasData"); + } + + [Fact] + public static void ExcessivelyPreciseFraction() + { + byte[] inputData = Text.Encoding.ASCII.GetBytes("\u0018\u002A2017092118.012345678901234567890123456789Z"); + + AsnReader berReader = new AsnReader(inputData, AsnEncodingRules.BER); + DateTimeOffset value = berReader.GetGeneralizedTime(); + Assert.False(berReader.HasData, "berReader.HasData"); + + DateTimeOffset expected = new DateTimeOffset(2017, 9, 21, 18, 0, 44, 444, TimeSpan.Zero); + + Assert.Equal(expected, value); + } + + [Fact] + public static void ExcessivelyPreciseFraction_OneTenthPlusEpsilon() + { + byte[] inputData = Text.Encoding.ASCII.GetBytes("\u0018\u002A20170921180044.10000000000000000000000001Z"); + + AsnReader derReader = new AsnReader(inputData, AsnEncodingRules.DER); + DateTimeOffset value = derReader.GetGeneralizedTime(); + Assert.False(derReader.HasData, "derReader.HasData"); + + DateTimeOffset expected = new DateTimeOffset(2017, 9, 21, 18, 0, 44, 100, TimeSpan.Zero); + + Assert.Equal(expected, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + public static void MultiSegmentExcessivelyPreciseFraction(PublicEncodingRules ruleSet) + { + // This builds "20171207173522.0000...0001Z" where the Z required a second CER segment. + // This is a bit of nonsense, really, because it is encoding 1e-985 seconds, which is + // oodles of orders of magnitude smaller than Planck time (~5e-44). + // But, the spec says "any number of decimal places", and 985 is a number. + + // A0 80 (context specifc 0, constructed, indefinite length) + // 04 82 03 E8 (octet string, primitive, 1000 bytes) + // ASCII("20171207173522." + new string('0', 984) + '1') + // 04 01 (octet string, primitive, 1 byte) + // ASCII("Z") + // 00 00 (end of contents) + // + // 1001 content bytes + 10 bytes of structure. + byte[] header = "A080048203E8".HexToByteArray(); + byte[] contents0 = Text.Encoding.ASCII.GetBytes("20171207173522." + new string('0', 984) + "1"); + byte[] cdr = { 0x04, 0x01, (byte)'Z', 0x00, 0x00 }; + byte[] inputData = header.Concat(contents0).Concat(cdr).ToArray(); + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + DateTimeOffset value = reader.GetGeneralizedTime(new Asn1Tag(TagClass.ContextSpecific, 0)); + DateTimeOffset expected = new DateTimeOffset(2017, 12, 7, 17, 35, 22, TimeSpan.Zero); + Assert.Equal(expected, value); + } + + [Fact] + public static void ExcessivelyPreciseFraction_OneTenthPlusEpsilonAndZero() + { + byte[] inputData = Text.Encoding.ASCII.GetBytes("\u0018\u002A20170921180044.10000000000000000000000010Z"); + + AsnReader berReader = new AsnReader(inputData, AsnEncodingRules.BER); + DateTimeOffset value = berReader.GetGeneralizedTime(); + Assert.False(berReader.HasData, "berReader.HasData"); + + DateTimeOffset expected = new DateTimeOffset(2017, 9, 21, 18, 0, 44, 100, TimeSpan.Zero); + Assert.Equal(expected, value); + + AsnReader cerReader = new AsnReader(inputData, AsnEncodingRules.CER); + AsnReader derReader = new AsnReader(inputData, AsnEncodingRules.DER); + Assert.Throws<CryptographicException>(() => cerReader.GetGeneralizedTime()); + Assert.Throws<CryptographicException>(() => derReader.GetGeneralizedTime()); + } + + [Fact] + public static void ExcessivelyPreciseNonFraction() + { + byte[] inputData = Text.Encoding.ASCII.GetBytes("\u0018\u002A2017092118.012345678901234567890123Q56789Z"); + AsnReader berReader = new AsnReader(inputData, AsnEncodingRules.BER); + + Assert.Throws<CryptographicException>(() => berReader.GetGeneralizedTime()); + } + + [Theory] + // yyyyMMddHH,hourFrac (2017090821,1) + [InlineData("180C323031373039303832312C31")] + // yyyyMMddHH.hourFrac (2017090821.1) + [InlineData("180C323031373039303832312E31")] + // yyyyMMddHH,hourFracZ (2017090821,2010Z) + [InlineData("1810323031373039303832312C323031305A")] + // yyyyMMddHH.hourFracZ (2017090821.2010Z) + [InlineData("1810323031373039303832312E323031305A")] + // yyyyMMddHH,hourFrac-HH (2017090821,3099-01) + [InlineData("1812323031373039303832312C333039392D3031")] + // yyyyMMddHH.hourFrac-HH (2017090821.3099-01) + [InlineData("1812323031373039303832312E333039392D3031")] + // yyyyMMddHH,hourFrac+HHmm (2017090821,201+0118) + [InlineData("1813323031373039303832312C3230312B30313138")] + // yyyyMMddHH.hourFrac+HHmm (2017090821.201+0118) + [InlineData("1813323031373039303832312E3230312B30313138")] + // yyyyMMddHHmm,minuteFrac (201709082358,01) + [InlineData("180F3230313730393038323335382C3031")] + // yyyyMMddHHmm.minuteFrac (201709082358.01) + [InlineData("180F3230313730393038323335382E3031")] + // yyyyMMddHHmm,minuteFracZ (201709082358,11Z) + [InlineData("18103230313730393038323335382C31315A")] + // yyyyMMddHHmm.minuteFracZ (201709082358.11Z) + [InlineData("18103230313730393038323335382E31315A")] + // yyyyMMddHHmm,minuteFrac-HH (201709082358m05-01) + [InlineData("18123230313730393038323335382C30352D3031")] + // yyyyMMddHHmm.minuteFrac-HH (201709082358.05-01) + [InlineData("18123230313730393038323335382E30352D3031")] + // yyyyMMddHHmm,minuteFrac+HHmm (201709082358,007+0118) + [InlineData("18153230313730393038323335382C3030372B30313138")] + // yyyyMMddHHmm.minuteFrac+HHmm (201709082358.007+0118) + [InlineData("18153230313730393038323335382E3030372B30313138")] + // yyyyMMddHHmmss,secondFrac (20161106012345,6789) + [InlineData("181332303136313130363031323334352C36373839")] + // yyyyMMddHHmmss.secondFrac (20161106012345.6789) + [InlineData("181332303136313130363031323334352E36373839")] + // yyyyMMddHHmmss,secondFracZ (20161106012345,7654Z) + [InlineData("181432303136313130363031323334352C373635345A")] + // yyyyMMddHHmmss,secondFrac-HH (20161106012345,001-01) + [InlineData("181532303136313130363031323334352C3030312D3031")] + // yyyyMMddHHmmss.secondFrac-HH (20161106012345.001-01) + [InlineData("181532303136313130363031323334352E3030312D3031")] + // yyyyMMddHHmmss,secondFrac+HHmm (20161106012345,0009+0118) + [InlineData("181832303136313130363031323334352C303030392B30313138")] + // yyyyMMddHHmmss.secondFrac+HHmm (20161106012345.0009+0118) + [InlineData("181832303136313130363031323334352E303030392B30313138")] + // yyyyMMddHHmmss.secondFrac0Z (20161106012345.76540Z) + [InlineData("181532303136313130363031323334352E37363534305A")] + public static void VerifyDisallowFraction_BER(string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + + AsnReader berReader = new AsnReader(inputData, AsnEncodingRules.BER); + + Assert.Throws<CryptographicException>(() => berReader.GetGeneralizedTime(disallowFractions: true)); + + // Legit if the fraction is allowed + berReader.GetGeneralizedTime(); + Assert.False(berReader.HasData, "berReader.HasData"); + } + + [Theory] + [InlineData("Wrong Tag", "170F32303136313130363031323334355A")] + [InlineData("Incomplete Tag", "1F")] + [InlineData("No Length", "18")] + [InlineData("No payload", "180F")] + [InlineData("Length exceeds content", "181032303136313130363031323334355A")] + [InlineData("yyyyMMdd", "18083230313631313036")] + [InlineData("yyyyMMddZ", "180932303136313130365A")] + [InlineData("yyyyMMdd+HH", "180B32303136313130362D3030")] + [InlineData("yyyyMMdd-HHmm", "180D32303136313130362B30303030")] + [InlineData("yyyyMMddH", "1809323031363131303630")] + [InlineData("yyyyMMddHZ", "180A3230313631313036305A")] + [InlineData("yQyyMMddHH", "180A32513136313130363031")] + [InlineData("yyyyMQddHH", "180A32303136315130363031")] + [InlineData("yyyyMMQdHH", "180A32303136313151363031")] + [InlineData("yyyyMMddHQ", "180A32303136313130363051")] + [InlineData("yyyyMMddHH,", "180B323031363131303630312C")] + [InlineData("yyyyMMddHH.", "180B323031363131303630312E")] + [InlineData("yyyyMMddHHQ", "180B3230313631313036303151")] + [InlineData("yyyyMMddHH,Q", "180C323031363131303630312C51")] + [InlineData("yyyyMMddHH.Q", "180C323031363131303630312E51")] + [InlineData("yyyyMMddHH..", "180C323031363131303630312E2E")] + [InlineData("yyyyMMddHH-", "180B323031363131303630312B")] + [InlineData("yyyyMMddHH+", "180B323031363131303630312D")] + [InlineData("yyyyMMddHHmQ", "180C323031363131303630313251")] + [InlineData("yyyyMMddHHm+", "180C32303136313130363031322D")] + [InlineData("yyyyMMddHHmmQ", "180D32303136313130363031323351")] + [InlineData("yyyyMMddHHmm-", "180D3230313631313036303132332B")] + [InlineData("yyyyMMddHHmm,", "180D3230313631313036303132332C")] + [InlineData("yyyyMMddHHmm+", "180D3230313631313036303132332D")] + [InlineData("yyyyMMddHHmm.", "180D3230313631313036303132332E")] + [InlineData("yyyyMMddHHmm+Q", "180E3230313631313036303132332D51")] + [InlineData("yyyyMMddHHmm.Q", "180E3230313631313036303132332E51")] + [InlineData("yyyyMMddHHmm..", "180E3230313631313036303132332E2E")] + [InlineData("yyyyMMddHHmm.ss,", "18103230313631313036303132332E31312E")] + [InlineData("yyyyMMddHHmms", "180D32303136313130363031323334")] + [InlineData("yyyyMMddHHmmsQ", "180E3230313631313036303132333451")] + [InlineData("yyyyMMddHHmmss-", "180F32303136313130363031323334352B")] + [InlineData("yyyyMMddHHmmss,", "180F32303136313130363031323334352C")] + [InlineData("yyyyMMddHHmmss+", "180F32303136313130363031323334352D")] + [InlineData("yyyyMMddHHmmss.", "180F32303136313130363031323334352E")] + [InlineData("yyyyMMddHHmmssQ", "180F323031363131303630313233343551")] + [InlineData("yyyyMMddHHmmss.Q", "181032303136313130363031323334352E51")] + [InlineData("yyyyMMddHHmmss.Q", "181032303136313130363031323334352E2E")] + [InlineData("yyyyMMddHHZmm", "180D323031363131303630315A3233")] + [InlineData("yyyyMMddHHmmZss", "180F3230313631313036303132335A3435")] + [InlineData("yyyyMMddHHmmssZ.s", "181232303136313130363031323334355A2E36")] + [InlineData("yyyyMMddHH+H", "180C323031363131303630312D30")] + [InlineData("yyyyMMddHH+HQ", "180D323031363131303630312D3051")] + [InlineData("yyyyMMddHH+HHQ", "180E323031363131303630312D303051")] + [InlineData("yyyyMMddHH+HHmQ", "180F323031363131303630312D30303051")] + [InlineData("yyyyMMddHH+HHmmQ", "1810323031363131303630312D3030303151")] + [InlineData("yyyyMMdd+H", "180A32303137313230382D30")] + [InlineData("yyyyMMddH+", "180A3230313731323038302D")] + [InlineData("yyyyMMddHH+0060", "180F323031373132303830392D30303630")] + [InlineData("yyyyMMddHH-2400", "180F323031373132303830392D32343030")] + [InlineData("yyyyMMddHH-HH:mm", "1810323031373039303832312D30313A3138")] + [InlineData("yyyyMMddHHmm-HH:mm", "18123230313730393038323131302D30313A3138")] + [InlineData("yyyyMMddHHmmss-HH:mm", "181432303137303930383231313032302D30313A3138")] + [InlineData("yyyyMMddHH,hourFrac-HH:mm", "1812323031373039303832312C312D30313A3138")] + [InlineData("yyyyMMddHH.hourFrac-HH:mm", "1812323031373039303832312E312D30313A3138")] + [InlineData("yyyyMMddHHmm,minuteFrac-HH:mm", "18183230313730393038323335382C30303030352D30313A3138")] + [InlineData("yyyyMMddHHmm.minuteFrac-HH:mm", "18183230313730393038323335382E30303030352D30313A3138")] + [InlineData("yyyyMMddHHmmss,secondFrac-HH:mm", "181932303136313130363031323334352C393939392D30313A3138")] + [InlineData("yyyyMMddHHmmss.secondFrac-HH:mm", "181932303136313130363031323334352E393939392D30313A3138")] + public static void GetGeneralizedTime_Throws(string description, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, AsnEncodingRules.BER); + + Assert.Throws<CryptographicException>(() => reader.GetGeneralizedTime()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = "180F32303136313130363031323334355A".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.GetGeneralizedTime(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.GetGeneralizedTime(new Asn1Tag(TagClass.ContextSpecific, 0))); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + Assert.Equal( + new DateTimeOffset(2016, 11, 6, 1, 23, 45, TimeSpan.Zero), + reader.GetGeneralizedTime()); + + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = "850F32303136313130363031323334355A".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.GetGeneralizedTime(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.GetUtcTime()); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.GetGeneralizedTime(new Asn1Tag(TagClass.Application, 5))); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.GetGeneralizedTime(new Asn1Tag(TagClass.ContextSpecific, 7))); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + Assert.Equal( + new DateTimeOffset(2016, 11, 6, 1, 23, 45, TimeSpan.Zero), + reader.GetGeneralizedTime(new Asn1Tag(TagClass.ContextSpecific, 5))); + + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "180F32303136313130363031323334355A", PublicTagClass.Universal, 24)] + [InlineData(PublicEncodingRules.CER, "180F32303136313130363031323334355A", PublicTagClass.Universal, 24)] + [InlineData(PublicEncodingRules.DER, "180F32303136313130363031323334355A", PublicTagClass.Universal, 24)] + [InlineData(PublicEncodingRules.BER, "800F31393530303130323132333435365A", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C0F31393530303130323132333435365A", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A460F31393530303130323132333435365A", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + DateTimeOffset val1 = reader.GetGeneralizedTime(new Asn1Tag((TagClass)tagClass, tagValue, true)); + + Assert.False(reader.HasData); + + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + DateTimeOffset val2 = reader.GetGeneralizedTime(new Asn1Tag((TagClass)tagClass, tagValue, false)); + + Assert.False(reader.HasData); + + Assert.Equal(val1, val2); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadIA5String.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadIA5String.cs new file mode 100644 index 0000000000..8319493513 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadIA5String.cs @@ -0,0 +1,721 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadIA5String : Asn1ReaderTests + { + public static IEnumerable<object[]> ValidEncodingData { get; } = + new object[][] + { + new object[] + { + PublicEncodingRules.BER, + "160D4A6F686E20512E20536D697468", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.CER, + "160D4A6F686E20512E20536D697468", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.DER, + "160D4A6F686E20512E20536D697468", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.BER, + "3680" + "040D4A6F686E20512E20536D697468" + "0000", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.BER, + "360F" + "040D4A6F686E20512E20536D697468", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.BER, + "1600", + "", + }, + new object[] + { + PublicEncodingRules.CER, + "1600", + "", + }, + new object[] + { + PublicEncodingRules.DER, + "1600", + "", + }, + new object[] + { + PublicEncodingRules.BER, + "3600", + "", + }, + new object[] + { + PublicEncodingRules.BER, + "3680" + "0000", + "", + }, + new object[] + { + PublicEncodingRules.BER, + "3680" + + "2480" + + // "Dr." + "040344722E" + + // " & " + "0403202620" + + // "Mrs." + "04044D72732E" + + "0000" + + // " " + "040120" + + "2480" + + "240A" + + // "Smith" + "0405536D697468" + + // hyphen (U+2010) is not valid, so use hyphen-minus + "04012D" + + "0000" + + // "Jones" + "04054A6F6E6573" + + "2480" + + // " " + "040120" + + "2480" + + // small ampersand (U+FE60) is not valid, so use ampersand. + "040126" + + "0000" + + // " " + "040120" + + // "children" + "04086368696C6472656E" + + "0000" + + "0000", + "Dr. & Mrs. Smith-Jones & children", + }, + }; + + [Theory] + [MemberData(nameof(ValidEncodingData))] + public static void GetIA5String_Success( + PublicEncodingRules ruleSet, + string inputHex, + string expectedValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + string value = reader.GetCharacterString(UniversalTagNumber.IA5String); + + Assert.Equal(expectedValue, value); + } + + [Theory] + [MemberData(nameof(ValidEncodingData))] + public static void TryCopyIA5String( + PublicEncodingRules ruleSet, + string inputHex, + string expectedValue) + { + byte[] inputData = inputHex.HexToByteArray(); + char[] output = new char[expectedValue.Length]; + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + bool copied; + int charsWritten; + + if (output.Length > 0) + { + output[0] = 'a'; + + copied = reader.TryCopyIA5String(output.AsSpan().Slice(0, expectedValue.Length - 1), + out charsWritten); + + Assert.False(copied, "reader.TryCopyIA5String - too short"); + Assert.Equal(0, charsWritten); + Assert.Equal('a', output[0]); + } + + copied = reader.TryCopyIA5String(output, + out charsWritten); + + Assert.True(copied, "reader.TryCopyIA5String"); + + string actualValue = new string(output, 0, charsWritten); + Assert.Equal(expectedValue, actualValue); + } + + [Theory] + [MemberData(nameof(ValidEncodingData))] + public static void TryCopyIA5StringBytes( + PublicEncodingRules ruleSet, + string inputHex, + string expectedString) + { + byte[] inputData = inputHex.HexToByteArray(); + string expectedHex = Text.Encoding.ASCII.GetBytes(expectedString).ByteArrayToHex(); + byte[] output = new byte[expectedHex.Length / 2]; + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + bool copied; + int bytesWritten; + + if (output.Length > 0) + { + output[0] = 32; + + copied = reader.TryCopyIA5StringBytes(output.AsSpan().Slice(0, output.Length - 1), + out bytesWritten); + + Assert.False(copied, "reader.TryCopyIA5StringBytes - too short"); + Assert.Equal(0, bytesWritten); + Assert.Equal(32, output[0]); + } + + copied = reader.TryCopyIA5StringBytes(output, + out bytesWritten); + + Assert.True(copied, "reader.TryCopyIA5StringBytes"); + + Assert.Equal( + expectedHex, + new ReadOnlySpan<byte>(output, 0, bytesWritten).ByteArrayToHex()); + + Assert.Equal(output.Length, bytesWritten); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "160120", true)] + [InlineData(PublicEncodingRules.BER, "3680" + "040120" + "0000", false)] + [InlineData(PublicEncodingRules.BER, "3603" + "040120", false)] + public static void TryGetIA5StringBytes( + PublicEncodingRules ruleSet, + string inputHex, + bool expectSuccess) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + bool got = reader.TryGetIA5StringBytes(out ReadOnlyMemory<byte> contents); + + if (expectSuccess) + { + Assert.True(got, "reader.TryGetIA5StringBytes"); + + Assert.True( + Unsafe.AreSame( + ref contents.Span.DangerousGetPinnableReference(), + ref inputData[2])); + } + else + { + Assert.False(got, "reader.TryGetIA5StringBytes"); + Assert.True(contents.IsEmpty, "contents.IsEmpty"); + } + } + + [Theory] + [InlineData("Incomplete Tag", PublicEncodingRules.BER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.CER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.DER, "1F")] + [InlineData("Missing Length", PublicEncodingRules.BER, "16")] + [InlineData("Missing Length", PublicEncodingRules.CER, "16")] + [InlineData("Missing Length", PublicEncodingRules.DER, "16")] + [InlineData("Missing Contents", PublicEncodingRules.BER, "1601")] + [InlineData("Missing Contents", PublicEncodingRules.CER, "1601")] + [InlineData("Missing Contents", PublicEncodingRules.DER, "1601")] + [InlineData("Length Too Long", PublicEncodingRules.BER, "16034869")] + [InlineData("Length Too Long", PublicEncodingRules.CER, "16034869")] + [InlineData("Length Too Long", PublicEncodingRules.DER, "16034869")] + [InlineData("Constructed Form", PublicEncodingRules.DER, "3603040149")] + public static void TryGetIA5StringBytes_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => reader.TryGetIA5StringBytes(out ReadOnlyMemory<byte> contents)); + } + + [Theory] + [InlineData("Empty", PublicEncodingRules.BER, "")] + [InlineData("Empty", PublicEncodingRules.CER, "")] + [InlineData("Empty", PublicEncodingRules.DER, "")] + [InlineData("Incomplete Tag", PublicEncodingRules.BER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.CER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.DER, "1F")] + [InlineData("Missing Length", PublicEncodingRules.BER, "16")] + [InlineData("Missing Length", PublicEncodingRules.CER, "16")] + [InlineData("Missing Length", PublicEncodingRules.DER, "16")] + [InlineData("Missing Contents", PublicEncodingRules.BER, "1601")] + [InlineData("Missing Contents", PublicEncodingRules.CER, "1601")] + [InlineData("Missing Contents", PublicEncodingRules.DER, "1601")] + [InlineData("Missing Contents - Constructed", PublicEncodingRules.BER, "3601")] + [InlineData("Missing Contents - Constructed Indef", PublicEncodingRules.BER, "3680")] + [InlineData("Missing Contents - Constructed Indef", PublicEncodingRules.CER, "3680")] + [InlineData("Length Too Long", PublicEncodingRules.BER, "16034869")] + [InlineData("Length Too Long", PublicEncodingRules.CER, "16034869")] + [InlineData("Length Too Long", PublicEncodingRules.DER, "16034869")] + [InlineData("Definite Constructed Form", PublicEncodingRules.CER, "3603040149")] + [InlineData("Definite Constructed Form", PublicEncodingRules.DER, "3603040149")] + [InlineData("Indefinite Constructed Form - Short Payload", PublicEncodingRules.CER, "36800401490000")] + [InlineData("Indefinite Constructed Form", PublicEncodingRules.DER, "36800401490000")] + [InlineData("No nested content", PublicEncodingRules.CER, "36800000")] + [InlineData("No EoC", PublicEncodingRules.BER, "3680" + "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.BER, "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.CER, "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.DER, "04024869")] + [InlineData("Wrong Tag - Constructed", PublicEncodingRules.BER, "240404024869")] + [InlineData("Wrong Tag - Constructed Indef", PublicEncodingRules.BER, "2480" + "04024869" + "0000")] + [InlineData("Wrong Tag - Constructed Indef", PublicEncodingRules.CER, "2480" + "04024869" + "0000")] + [InlineData("Wrong Tag - Constructed", PublicEncodingRules.DER, "240404024869")] + [InlineData("Nested Bad Tag", PublicEncodingRules.BER, "3604" + "16024869")] + [InlineData("Nested context-specific", PublicEncodingRules.BER, "3604800400FACE")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.BER, "3680800400FACE0000")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.CER, "3680800400FACE0000")] + [InlineData("Nested Length Too Long", PublicEncodingRules.BER, "3607" + ("2402" + "0403") + "040149")] + [InlineData("Nested Simple Length Too Long", PublicEncodingRules.BER, "3603" + "040548656C6C6F")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.BER, "368020000000")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.CER, "368020000000")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.BER, "3680000100")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.CER, "3680000100")] + [InlineData("LongLength EndOfContents", PublicEncodingRules.BER, "3680008100")] + public static void TryCopyIA5StringBytes_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + byte[] outputData = new byte[inputData.Length + 1]; + outputData[0] = 252; + + int bytesWritten = -1; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => reader.TryCopyIA5StringBytes(outputData, out bytesWritten)); + + Assert.Equal(-1, bytesWritten); + Assert.Equal(252, outputData[0]); + } + + private static void TryCopyIA5String_Throws(PublicEncodingRules ruleSet, byte[] inputData) + { + char[] outputData = new char[inputData.Length + 1]; + outputData[0] = 'a'; + + int bytesWritten = -1; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => reader.TryCopyIA5String(outputData, out bytesWritten)); + + Assert.Equal(-1, bytesWritten); + Assert.Equal('a', outputData[0]); + } + + [Theory] + [InlineData("Bad IA5 value", PublicEncodingRules.BER, "1602E280")] + [InlineData("Bad IA5 value", PublicEncodingRules.CER, "1602E280")] + [InlineData("Bad IA5 value", PublicEncodingRules.DER, "1602E280")] + [InlineData("Wrong Tag", PublicEncodingRules.BER, "04024869")] + public static void GetIA5String_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => reader.GetCharacterString(UniversalTagNumber.IA5String)); + } + + [Theory] + [InlineData("Empty", PublicEncodingRules.BER, "")] + [InlineData("Empty", PublicEncodingRules.CER, "")] + [InlineData("Empty", PublicEncodingRules.DER, "")] + [InlineData("Incomplete Tag", PublicEncodingRules.BER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.CER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.DER, "1F")] + [InlineData("Missing Length", PublicEncodingRules.BER, "16")] + [InlineData("Missing Length", PublicEncodingRules.CER, "16")] + [InlineData("Missing Length", PublicEncodingRules.DER, "16")] + [InlineData("Missing Contents", PublicEncodingRules.BER, "1601")] + [InlineData("Missing Contents", PublicEncodingRules.CER, "1601")] + [InlineData("Missing Contents", PublicEncodingRules.DER, "1601")] + [InlineData("Missing Contents - Constructed", PublicEncodingRules.BER, "3601")] + [InlineData("Missing Contents - Constructed Indef", PublicEncodingRules.BER, "3680")] + [InlineData("Missing Contents - Constructed Indef", PublicEncodingRules.CER, "3680")] + [InlineData("Length Too Long", PublicEncodingRules.BER, "16034869")] + [InlineData("Length Too Long", PublicEncodingRules.CER, "16034869")] + [InlineData("Length Too Long", PublicEncodingRules.DER, "16034869")] + [InlineData("Definite Constructed Form", PublicEncodingRules.CER, "3603040149")] + [InlineData("Definite Constructed Form", PublicEncodingRules.DER, "3603040149")] + [InlineData("Indefinite Constructed Form - Short Payload", PublicEncodingRules.CER, "36800401490000")] + [InlineData("Indefinite Constructed Form", PublicEncodingRules.DER, "36800401490000")] + [InlineData("No nested content", PublicEncodingRules.CER, "36800000")] + [InlineData("No EoC", PublicEncodingRules.BER, "3680" + "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.BER, "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.CER, "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.DER, "04024869")] + [InlineData("Wrong Tag - Constructed", PublicEncodingRules.BER, "240404024869")] + [InlineData("Wrong Tag - Constructed Indef", PublicEncodingRules.BER, "2480" + "04024869" + "0000")] + [InlineData("Wrong Tag - Constructed Indef", PublicEncodingRules.CER, "2480" + "04024869" + "0000")] + [InlineData("Wrong Tag - Constructed", PublicEncodingRules.DER, "240404024869")] + [InlineData("Nested Bad Tag", PublicEncodingRules.BER, "3604" + "16024869")] + [InlineData("Nested context-specific", PublicEncodingRules.BER, "3604800400FACE")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.BER, "3680800400FACE0000")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.CER, "3680800400FACE0000")] + [InlineData("Nested Length Too Long", PublicEncodingRules.BER, "3607" + ("2402" + "0403") + "040149")] + [InlineData("Nested Simple Length Too Long", PublicEncodingRules.BER, "3603" + "040548656C6C6F")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.BER, "368020000000")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.CER, "368020000000")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.BER, "3680000100")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.CER, "3680000100")] + [InlineData("LongLength EndOfContents", PublicEncodingRules.BER, "3680008100")] + [InlineData("Bad IA5 value", PublicEncodingRules.BER, "1602E280")] + public static void TryCopyIA5String_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + TryCopyIA5String_Throws(ruleSet, inputData); + } + + [Fact] + public static void TryCopyIA5String_Throws_CER_NestedTooLong() + { + // CER says that the maximum encoding length for a IA5String primitive + // is 1000. + // + // This test checks it for a primitive contained within a constructed. + // + // So we need 04 [1001] { 1001 0x00s } + // 1001 => 0x3E9, so the length encoding is 82 03 E9. + // 1001 + 3 + 1 == 1005 + // + // Plus a leading 36 80 (indefinite length constructed) + // and a trailing 00 00 (End of contents) + // == 1009 + byte[] input = new byte[1009]; + // CONSTRUCTED IA5STRING (indefinite) + input[0] = 0x36; + input[1] = 0x80; + // OCTET STRING (1001) + input[2] = 0x04; + input[3] = 0x82; + input[4] = 0x03; + input[5] = 0xE9; + // EOC implicit since the byte[] initializes to zeros + + TryCopyIA5String_Throws(PublicEncodingRules.CER, input); + } + + [Fact] + public static void TryCopyIA5String_Throws_CER_NestedTooShortIntermediate() + { + // CER says that the maximum encoding length for a IA5String primitive + // is 1000, and in the constructed form the lengths must be + // [ 1000, 1000, 1000, ..., len%1000 ] + // + // So 1000, 2, 2 is illegal. + // + // 36 80 (indefinite constructed IA5 string) + // 04 82 03 08 (octet string, 1000 bytes) + // [1000 content bytes] + // 04 02 (octet string, 2 bytes) + // [2 content bytes] + // 04 02 (octet string, 2 bytes) + // [2 content bytes] + // 00 00 (end of contents) + // Looks like 1,016 bytes. + byte[] input = new byte[1016]; + // CONSTRUCTED IA5STRING (indefinite) + input[0] = 0x36; + input[1] = 0x80; + // OCTET STRING (1000) + input[2] = 0x03; + input[3] = 0x82; + input[4] = 0x03; + input[5] = 0xE8; + // OCTET STRING (2) + input[1006] = 0x04; + input[1007] = 0x02; + // OCTET STRING (2) + input[1010] = 0x04; + input[1011] = 0x02; + // EOC implicit since the byte[] initializes to zeros + + TryCopyIA5String_Throws(PublicEncodingRules.CER, input); + } + + [Fact] + public static void TryCopyIA5StringBytes_Success_CER_MaxPrimitiveLength() + { + // CER says that the maximum encoding length for a IA5String primitive + // is 1000. + // + // So we need 16 [1000] { 1000 anythings } + // 1000 => 0x3E8, so the length encoding is 82 03 E8. + // 1000 + 3 + 1 == 1004 + byte[] input = new byte[1004]; + input[0] = 0x16; + input[1] = 0x82; + input[2] = 0x03; + input[3] = 0xE8; + + // Content + input[4] = 0x65; + input[5] = 0x65; + input[1002] = 0x61; + input[1003] = 0x61; + + byte[] output = new byte[1000]; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + + bool success = reader.TryCopyIA5StringBytes(output, + out int bytesWritten); + + Assert.True(success, "reader.TryCopyIA5StringBytes"); + Assert.Equal(1000, bytesWritten); + + Assert.Equal( + input.AsReadOnlySpan().Slice(4).ByteArrayToHex(), + output.ByteArrayToHex()); + } + + [Fact] + public static void TryCopyIA5StringBytes_Success_CER_MinConstructedLength() + { + // CER says that the maximum encoding length for a IA5String primitive + // is 1000, and that a constructed form must be used for values greater + // than 1000 bytes, with segments dividing up for each thousand + // [1000, 1000, ..., len%1000]. + // + // So our smallest constructed form is 1001 bytes, [1000, 1] + // + // 36 80 (indefinite constructed IA5 string) + // 04 82 03 E9 (primitive octet string, 1000 bytes) + // [1000 content bytes] + // 04 01 (primitive octet string, 1 byte) + // pp + // 00 00 (end of contents, 0 bytes) + // 1011 total. + byte[] input = new byte[1011]; + int offset = 0; + // CONSTRUCTED IA5STRING (Indefinite) + input[offset++] = 0x36; + input[offset++] = 0x80; + // OCTET STRING (1000) + input[offset++] = 0x04; + input[offset++] = 0x82; + input[offset++] = 0x03; + input[offset++] = 0xE8; + + // Primitive 1: (65 65 :: 61 61) (1000) + input[offset++] = 0x65; + input[offset] = 0x65; + offset += 997; + input[offset++] = 0x61; + input[offset++] = 0x61; + + // OCTET STRING (1) + input[offset++] = 0x04; + input[offset++] = 0x01; + + // Primitive 2: One more byte + input[offset] = 0x2E; + + byte[] expected = new byte[1001]; + offset = 0; + expected[offset++] = 0x65; + expected[offset] = 0x65; + offset += 997; + expected[offset++] = 0x61; + expected[offset++] = 0x61; + expected[offset] = 0x2E; + + byte[] output = new byte[1001]; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + + bool success = reader.TryCopyIA5StringBytes(output, + out int bytesWritten); + + Assert.True(success, "reader.TryCopyIA5StringBytes"); + Assert.Equal(1001, bytesWritten); + + Assert.Equal( + expected.ByteArrayToHex(), + output.ByteArrayToHex()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x16, 2, (byte)'e', (byte)'l' }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.TryGetIA5StringBytes(Asn1Tag.Null, out _)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetIA5StringBytes(new Asn1Tag(TagClass.ContextSpecific, 0), out _)); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + Assert.True(reader.TryGetIA5StringBytes(out ReadOnlyMemory<byte> value)); + Assert.Equal("656C", value.ByteArrayToHex()); + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x87, 2, (byte)'h', (byte)'i' }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.TryGetIA5StringBytes(Asn1Tag.Null, out _)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.TryGetIA5StringBytes(out _)); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetIA5StringBytes(new Asn1Tag(TagClass.Application, 0), out _)); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetIA5StringBytes(new Asn1Tag(TagClass.ContextSpecific, 1), out _)); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + Assert.True( + reader.TryGetIA5StringBytes( + new Asn1Tag(TagClass.ContextSpecific, 7), + out ReadOnlyMemory<byte> value)); + + Assert.Equal("6869", value.ByteArrayToHex()); + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "16026869", PublicTagClass.Universal, 22)] + [InlineData(PublicEncodingRules.CER, "16026869", PublicTagClass.Universal, 22)] + [InlineData(PublicEncodingRules.DER, "16026869", PublicTagClass.Universal, 22)] + [InlineData(PublicEncodingRules.BER, "80023132", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C023132", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A46023132", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.True( + reader.TryGetIA5StringBytes( + new Asn1Tag((TagClass)tagClass, tagValue, true), + out ReadOnlyMemory<byte> val1)); + + Assert.False(reader.HasData); + + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.True( + reader.TryGetIA5StringBytes( + new Asn1Tag((TagClass)tagClass, tagValue, false), + out ReadOnlyMemory<byte> val2)); + + Assert.False(reader.HasData); + + Assert.Equal(val1.ByteArrayToHex(), val2.ByteArrayToHex()); + } + } + + internal static class ReaderIA5Extensions + { + public static bool TryGetIA5StringBytes( + this AsnReader reader, + out ReadOnlyMemory<byte> contents) + { + return reader.TryGetPrimitiveCharacterStringBytes( + UniversalTagNumber.IA5String, + out contents); + } + + public static bool TryGetIA5StringBytes( + this AsnReader reader, + Asn1Tag expectedTag, + out ReadOnlyMemory<byte> contents) + { + return reader.TryGetPrimitiveCharacterStringBytes( + expectedTag, + UniversalTagNumber.IA5String, + out contents); + } + + public static bool TryCopyIA5StringBytes( + this AsnReader reader, + Span<byte> destination, + out int bytesWritten) + { + return reader.TryCopyCharacterStringBytes( + UniversalTagNumber.IA5String, + destination, + out bytesWritten); + } + + public static bool TryCopyIA5String( + this AsnReader reader, + Span<char> destination, + out int charsWritten) + { + return reader.TryCopyCharacterString( + UniversalTagNumber.IA5String, + destination, + out charsWritten); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadInteger.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadInteger.cs new file mode 100644 index 0000000000..6cf2e754d6 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadInteger.cs @@ -0,0 +1,498 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadInteger : Asn1ReaderTests + { + [Theory] + [InlineData("Constructed Encoding", PublicEncodingRules.BER, "2203020100")] + [InlineData("Constructed Encoding-Indefinite", PublicEncodingRules.BER, "228002010000")] + [InlineData("Constructed Encoding-Indefinite", PublicEncodingRules.CER, "228002010000")] + [InlineData("Constructed Encoding", PublicEncodingRules.DER, "2203020100")] + [InlineData("Wrong Universal Tag", PublicEncodingRules.BER, "030100")] + [InlineData("Bad Length", PublicEncodingRules.BER, "02030102")] + [InlineData("Incorrect Zero Encoding", PublicEncodingRules.BER, "0200")] + [InlineData("Incorrect Zero Encoding", PublicEncodingRules.CER, "0200")] + [InlineData("Incorrect Zero Encoding", PublicEncodingRules.DER, "0200")] + [InlineData("Redundant Leading 0x00", PublicEncodingRules.BER, "0202007F")] + [InlineData("Redundant Leading 0x00", PublicEncodingRules.CER, "0202007F")] + [InlineData("Redundant Leading 0x00", PublicEncodingRules.DER, "0202007F")] + [InlineData("Redundant Leading 0xFF", PublicEncodingRules.BER, "0202FF80")] + [InlineData("Redundant Leading 0xFF", PublicEncodingRules.CER, "0202FF80")] + [InlineData("Redundant Leading 0xFF", PublicEncodingRules.DER, "0202FF80")] + public static void InvalidData( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.GetIntegerBytes()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "020100", 0)] + [InlineData(PublicEncodingRules.CER, "020100", 0)] + [InlineData(PublicEncodingRules.DER, "020100", 0)] + [InlineData(PublicEncodingRules.DER, "02017F", sbyte.MaxValue)] + [InlineData(PublicEncodingRules.DER, "020180", sbyte.MinValue)] + [InlineData(PublicEncodingRules.DER, "0201FF", -1)] + public static void ReadInt8_Success( + PublicEncodingRules ruleSet, + string inputHex, + sbyte expectedValue) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadInt8(out sbyte value); + + Assert.True(didRead, "reader.TryReadInt8"); + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "02020102")] + [InlineData(PublicEncodingRules.CER, "02020102")] + [InlineData(PublicEncodingRules.DER, "02020102")] + public static void ReadInt8_TooMuchData( + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadInt8(out sbyte value); + + Assert.False(didRead, "reader.TryReadInt8"); + Assert.Equal(0, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "020100", 0)] + [InlineData(PublicEncodingRules.CER, "02017F", 0x7F)] + [InlineData(PublicEncodingRules.CER, "02020080", 0x80)] + [InlineData(PublicEncodingRules.CER, "020200FF", 0xFF)] + public static void ReadUInt8_Success( + PublicEncodingRules ruleSet, + string inputHex, + byte expectedValue) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadUInt8(out byte value); + + Assert.True(didRead, "reader.TryReadUInt8"); + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "020180")] + [InlineData(PublicEncodingRules.CER, "020180")] + [InlineData(PublicEncodingRules.DER, "020180")] + [InlineData(PublicEncodingRules.BER, "0201FF")] + [InlineData(PublicEncodingRules.CER, "0201FF")] + [InlineData(PublicEncodingRules.DER, "0201FF")] + public static void ReadUInt8_Failure(PublicEncodingRules ruleSet, string inputHex) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadUInt8(out byte value); + + Assert.False(didRead, "reader.TryReadUInt8"); + Assert.Equal((byte)0, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "020100", 0)] + [InlineData(PublicEncodingRules.CER, "020100", 0)] + [InlineData(PublicEncodingRules.DER, "020100", 0)] + [InlineData(PublicEncodingRules.DER, "0201FF", -1)] + [InlineData(PublicEncodingRules.CER, "0202FEFF", unchecked((short)0xFEFF))] + [InlineData(PublicEncodingRules.BER, "028102FEEF", unchecked((short)0xFEEF))] + [InlineData(PublicEncodingRules.BER, "0281028000", short.MinValue)] + [InlineData(PublicEncodingRules.CER, "02028000", short.MinValue)] + [InlineData(PublicEncodingRules.DER, "02027FFF", short.MaxValue)] + [InlineData(PublicEncodingRules.DER, "02026372", 0x6372)] + [InlineData(PublicEncodingRules.CER, "0202008A", 0x8A)] + [InlineData(PublicEncodingRules.CER, "02028ACE", unchecked((short)0x8ACE))] + public static void ReadInt16_Success( + PublicEncodingRules ruleSet, + string inputHex, + short expectedValue) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadInt16(out short value); + + Assert.True(didRead, "reader.TryReadInt16"); + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "0203010203")] + [InlineData(PublicEncodingRules.CER, "0203010203")] + [InlineData(PublicEncodingRules.DER, "0203010203")] + public static void ReadInt16_TooMuchData( + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadInt16(out short value); + + Assert.False(didRead, "reader.TryReadInt16"); + Assert.Equal(0, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "020100", 0)] + [InlineData(PublicEncodingRules.CER, "02020080", 0x80)] + [InlineData(PublicEncodingRules.DER, "02027F80", 0x7F80)] + [InlineData(PublicEncodingRules.DER, "0203008180", 0x8180)] + public static void ReadUInt16_Success( + PublicEncodingRules ruleSet, + string inputHex, + ushort expectedValue) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadUInt16(out ushort value); + + Assert.True(didRead, "reader.TryReadUInt16"); + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "020180")] + [InlineData(PublicEncodingRules.CER, "020180")] + [InlineData(PublicEncodingRules.DER, "020180")] + [InlineData(PublicEncodingRules.BER, "0201FF")] + [InlineData(PublicEncodingRules.CER, "0201FF")] + [InlineData(PublicEncodingRules.DER, "0201FF")] + [InlineData(PublicEncodingRules.DER, "02028000")] + public static void ReadUInt16_Failure(PublicEncodingRules ruleSet, string inputHex) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadUInt16(out ushort value); + + Assert.False(didRead, "reader.TryReadUInt16"); + Assert.Equal((ushort)0, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "020100", 0)] + [InlineData(PublicEncodingRules.CER, "020100", 0)] + [InlineData(PublicEncodingRules.DER, "020100", 0)] + [InlineData(PublicEncodingRules.DER, "0201FF", -1)] + [InlineData(PublicEncodingRules.CER, "0202FEFF", unchecked((int)0xFFFF_FEFF))] + [InlineData(PublicEncodingRules.BER, "028102FEEF", unchecked((int)0xFFFF_FEEF))] + [InlineData(PublicEncodingRules.BER, "02810480000000", int.MinValue)] + [InlineData(PublicEncodingRules.CER, "020480000000", int.MinValue)] + [InlineData(PublicEncodingRules.DER, "02047FFFFFFF", int.MaxValue)] + [InlineData(PublicEncodingRules.DER, "02026372", 0x6372)] + [InlineData(PublicEncodingRules.CER, "0203008ACE", 0x8ACE)] + [InlineData(PublicEncodingRules.BER, "0203FACE01", unchecked((int)0xFFFA_CE01))] + [InlineData(PublicEncodingRules.BER, "02820003FACE01", unchecked((int)0xFFFA_CE01))] + public static void ReadInt32_Success( + PublicEncodingRules ruleSet, + string inputHex, + int expectedValue) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadInt32(out int value); + + Assert.True(didRead, "reader.TryReadInt32"); + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "02050102030405")] + [InlineData(PublicEncodingRules.CER, "02050102030405")] + [InlineData(PublicEncodingRules.DER, "02050102030405")] + public static void ReadInt32_TooMuchData( + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadInt32(out int value); + + Assert.False(didRead, "reader.TryReadInt32"); + Assert.Equal(0, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "020100", 0)] + [InlineData(PublicEncodingRules.CER, "02020080", 0x80)] + [InlineData(PublicEncodingRules.DER, "02027F80", 0x7F80)] + [InlineData(PublicEncodingRules.DER, "0203008180", 0x8180)] + [InlineData(PublicEncodingRules.DER, "02030A8180", 0xA8180)] + [InlineData(PublicEncodingRules.DER, "020400828180", 0x828180)] + [InlineData(PublicEncodingRules.DER, "020475828180", 0x75828180)] + [InlineData(PublicEncodingRules.DER, "02050083828180", 0x83828180)] + [InlineData(PublicEncodingRules.BER, "02830000050083828180", 0x83828180)] + public static void ReadUInt32_Success( + PublicEncodingRules ruleSet, + string inputHex, + uint expectedValue) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadUInt32(out uint value); + + Assert.True(didRead, "reader.TryReadUInt32"); + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "020180")] + [InlineData(PublicEncodingRules.CER, "020180")] + [InlineData(PublicEncodingRules.DER, "020180")] + [InlineData(PublicEncodingRules.BER, "0201FF")] + [InlineData(PublicEncodingRules.CER, "0201FF")] + [InlineData(PublicEncodingRules.DER, "0201FF")] + [InlineData(PublicEncodingRules.DER, "02028000")] + [InlineData(PublicEncodingRules.DER, "0203800000")] + [InlineData(PublicEncodingRules.DER, "020480000000")] + [InlineData(PublicEncodingRules.DER, "02050100000000")] + public static void ReadUInt32_Failure(PublicEncodingRules ruleSet, string inputHex) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadUInt32(out uint value); + + Assert.False(didRead, "reader.TryReadUInt32"); + Assert.Equal((uint)0, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "020100", 0)] + [InlineData(PublicEncodingRules.CER, "02020080", 0x80)] + [InlineData(PublicEncodingRules.DER, "02027F80", 0x7F80)] + [InlineData(PublicEncodingRules.DER, "0203008180", 0x8180)] + [InlineData(PublicEncodingRules.DER, "02030A8180", 0xA8180)] + [InlineData(PublicEncodingRules.DER, "020400828180", 0x828180)] + [InlineData(PublicEncodingRules.DER, "020475828180", 0x75828180)] + [InlineData(PublicEncodingRules.DER, "02050083828180", 0x83828180)] + [InlineData(PublicEncodingRules.BER, "02830000050083828180", 0x83828180)] + [InlineData(PublicEncodingRules.DER, "02050183828180", 0x0183828180)] + [InlineData(PublicEncodingRules.DER, "0206018483828180", 0x018483828180)] + [InlineData(PublicEncodingRules.DER, "020701858483828180", 0x01858483828180)] + [InlineData(PublicEncodingRules.DER, "02080186858483828180", 0x0186858483828180)] + [InlineData(PublicEncodingRules.DER, "02087F86858483828180", 0x7F86858483828180)] + [InlineData(PublicEncodingRules.DER, "02087FFFFFFFFFFFFFFF", long.MaxValue)] + [InlineData(PublicEncodingRules.DER, "0201FF", -1)] + [InlineData(PublicEncodingRules.DER, "0201FE", -2)] + [InlineData(PublicEncodingRules.DER, "02028012", unchecked((long)0xFFFFFFFF_FFFF8012))] + [InlineData(PublicEncodingRules.DER, "0203818012", unchecked((long)0xFFFFFFFF_FF818012))] + [InlineData(PublicEncodingRules.DER, "020482818012", unchecked((long)0xFFFFFFFF_82818012))] + [InlineData(PublicEncodingRules.DER, "02058382818012", unchecked((long)0xFFFFFF83_82818012))] + [InlineData(PublicEncodingRules.DER, "0206848382818012", unchecked((long)0xFFFF8483_82818012))] + [InlineData(PublicEncodingRules.DER, "020785848382818012", unchecked((long)0xFF858483_82818012))] + [InlineData(PublicEncodingRules.DER, "02088685848382818012", unchecked((long)0x86858483_82818012))] + [InlineData(PublicEncodingRules.DER, "02088000000000000000", long.MinValue)] + [InlineData(PublicEncodingRules.BER, "028800000000000000088000000000000000", long.MinValue)] + public static void ReadInt64_Success( + PublicEncodingRules ruleSet, + string inputHex, + long expectedValue) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadInt64(out long value); + + Assert.True(didRead, "reader.TryReadInt64"); + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "0209010203040506070809")] + [InlineData(PublicEncodingRules.CER, "0209010203040506070809")] + [InlineData(PublicEncodingRules.DER, "0209010203040506070809")] + public static void ReadInt64_TooMuchData( + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadInt64(out long value); + + Assert.False(didRead, "reader.TryReadInt64"); + Assert.Equal(0, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "020100", 0)] + [InlineData(PublicEncodingRules.CER, "02020080", 0x80)] + [InlineData(PublicEncodingRules.DER, "02027F80", 0x7F80)] + [InlineData(PublicEncodingRules.DER, "0203008180", 0x8180)] + [InlineData(PublicEncodingRules.DER, "02030A8180", 0xA8180)] + [InlineData(PublicEncodingRules.DER, "020400828180", 0x828180)] + [InlineData(PublicEncodingRules.DER, "020475828180", 0x75828180)] + [InlineData(PublicEncodingRules.DER, "02050083828180", 0x83828180)] + [InlineData(PublicEncodingRules.BER, "02830000050083828180", 0x83828180)] + [InlineData(PublicEncodingRules.DER, "02050183828180", 0x0183828180)] + [InlineData(PublicEncodingRules.DER, "0206018483828180", 0x018483828180)] + [InlineData(PublicEncodingRules.DER, "020701858483828180", 0x01858483828180)] + [InlineData(PublicEncodingRules.DER, "02080186858483828180", 0x0186858483828180)] + [InlineData(PublicEncodingRules.DER, "02087F86858483828180", 0x7F86858483828180)] + [InlineData(PublicEncodingRules.DER, "02087FFFFFFFFFFFFFFF", long.MaxValue)] + [InlineData(PublicEncodingRules.DER, "0209008000000000000000", 0x80000000_00000000)] + [InlineData(PublicEncodingRules.DER, "020900FFFFFFFFFFFFFFFF", ulong.MaxValue)] + public static void ReadUInt64_Success( + PublicEncodingRules ruleSet, + string inputHex, + ulong expectedValue) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadUInt64(out ulong value); + + Assert.True(didRead, "reader.TryReadUInt64"); + Assert.Equal(expectedValue, value); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "020180")] + [InlineData(PublicEncodingRules.CER, "020180")] + [InlineData(PublicEncodingRules.DER, "020180")] + [InlineData(PublicEncodingRules.BER, "0201FF")] + [InlineData(PublicEncodingRules.CER, "0201FF")] + [InlineData(PublicEncodingRules.DER, "0201FF")] + [InlineData(PublicEncodingRules.DER, "02028000")] + [InlineData(PublicEncodingRules.DER, "0203800000")] + [InlineData(PublicEncodingRules.DER, "020480000000")] + [InlineData(PublicEncodingRules.DER, "02058000000000")] + [InlineData(PublicEncodingRules.DER, "0206800000000000")] + [InlineData(PublicEncodingRules.DER, "020780000000000000")] + [InlineData(PublicEncodingRules.DER, "02088000000000000000")] + [InlineData(PublicEncodingRules.DER, "0209010000000000000000")] + public static void ReadUInt64_Failure(PublicEncodingRules ruleSet, string inputHex) + { + byte[] data = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryReadUInt64(out ulong value); + + Assert.False(didRead, "reader.TryReadUInt64"); + Assert.Equal((uint)0, value); + } + + [Fact] + public static void GetIntegerBytes() + { + const string Payload = "0102030405060708090A0B0C0D0E0F10"; + + // INTEGER (payload) followed by INTEGER (0) + byte[] data = ("0210" + Payload + "020100").HexToByteArray(); + AsnReader reader = new AsnReader(data, AsnEncodingRules.DER); + + ReadOnlyMemory<byte> contents = reader.GetIntegerBytes(); + Assert.Equal(0x10, contents.Length); + Assert.Equal(Payload, contents.ByteArrayToHex()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = { 2, 1, 0x7E }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.GetIntegerBytes(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.GetIntegerBytes(new Asn1Tag(TagClass.ContextSpecific, 0))); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + ReadOnlyMemory<byte> value = reader.GetIntegerBytes(); + Assert.Equal("7E", value.ByteArrayToHex()); + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x87, 2, 0, 0x80 }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.GetIntegerBytes(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.GetIntegerBytes()); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>(() => reader.GetIntegerBytes(new Asn1Tag(TagClass.Application, 0))); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>(() => reader.GetIntegerBytes(new Asn1Tag(TagClass.ContextSpecific, 1))); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + ReadOnlyMemory<byte> value = reader.GetIntegerBytes(new Asn1Tag(TagClass.ContextSpecific, 7)); + Assert.Equal("0080", value.ByteArrayToHex()); + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "0201FF", PublicTagClass.Universal, 2)] + [InlineData(PublicEncodingRules.CER, "0201FF", PublicTagClass.Universal, 2)] + [InlineData(PublicEncodingRules.DER, "0201FF", PublicTagClass.Universal, 2)] + [InlineData(PublicEncodingRules.BER, "8001FF", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C01FF", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A4601FF", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + ReadOnlyMemory<byte> val1 = reader.GetIntegerBytes(new Asn1Tag((TagClass)tagClass, tagValue, true)); + Assert.False(reader.HasData); + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + ReadOnlyMemory<byte> val2 = reader.GetIntegerBytes(new Asn1Tag((TagClass)tagClass, tagValue, false)); + Assert.False(reader.HasData); + + Assert.Equal(val1.ByteArrayToHex(), val2.ByteArrayToHex()); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadLength.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadLength.cs new file mode 100644 index 0000000000..5b8ebe9574 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadLength.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadLength : Asn1ReaderTests + { + [Theory] + [InlineData(4, 0, "0400")] + [InlineData(1, 1, "0101")] + [InlineData(4, 127, "047F")] + [InlineData(4, 128, "048180")] + [InlineData(4, 255, "0481FF")] + [InlineData(2, 256, "02820100")] + [InlineData(4, int.MaxValue, "04847FFFFFFF")] + public static void MinimalPrimitiveLength(int tagValue, int length, string inputHex) + { + byte[] inputBytes = inputHex.HexToByteArray(); + + foreach (PublicEncodingRules rules in Enum.GetValues(typeof(PublicEncodingRules))) + { + AsnReader reader = new AsnReader(inputBytes, (AsnEncodingRules)rules); + + Asn1Tag tag = reader.ReadTagAndLength(out int ? parsedLength, out int bytesRead); + + Assert.Equal(inputBytes.Length, bytesRead); + Assert.False(tag.IsConstructed, "tag.IsConstructed"); + Assert.Equal(tagValue, tag.TagValue); + Assert.Equal(length, parsedLength.Value); + + // ReadTagAndLength doesn't move the _data span forward. + Assert.True(reader.HasData, "reader.HasData"); + } + } + + [Theory] + [InlineData(-1)] + [InlineData(3)] + public static void ReadWithUnknownRuleSet(int invalidRuleSetValue) + { + byte[] data = { 0x05, 0x00 }; + + Assert.Throws<ArgumentOutOfRangeException>( + () => new AsnReader(data, (AsnEncodingRules)invalidRuleSetValue)); + } + + [Theory] + [InlineData("")] + [InlineData("05")] + [InlineData("0481")] + [InlineData("048201")] + [InlineData("04830102")] + [InlineData("0484010203")] + public static void ReadWithInsufficientData(string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, AsnEncodingRules.DER); + + Assert.Throws<CryptographicException>(() => reader.ReadTagAndLength(out _, out _)); + } + + [Theory] + [InlineData("DER indefinite constructed", PublicEncodingRules.DER, "3080" + "0500" + "0000")] + [InlineData("0xFF-BER", PublicEncodingRules.BER, "04FF")] + [InlineData("0xFF-CER", PublicEncodingRules.CER, "04FF")] + [InlineData("0xFF-DER", PublicEncodingRules.DER, "04FF")] + [InlineData("CER definite constructed", PublicEncodingRules.CER, "30820500")] + [InlineData("BER indefinite primitive", PublicEncodingRules.BER, "0480" + "0000")] + [InlineData("CER indefinite primitive", PublicEncodingRules.CER, "0480" + "0000")] + [InlineData("DER indefinite primitive", PublicEncodingRules.DER, "0480" + "0000")] + [InlineData("DER non-minimal 0", PublicEncodingRules.DER, "048100")] + [InlineData("DER non-minimal 7F", PublicEncodingRules.DER, "04817F")] + [InlineData("DER non-minimal 80", PublicEncodingRules.DER, "04820080")] + [InlineData("CER non-minimal 0", PublicEncodingRules.CER, "048100")] + [InlineData("CER non-minimal 7F", PublicEncodingRules.CER, "04817F")] + [InlineData("CER non-minimal 80", PublicEncodingRules.CER, "04820080")] + [InlineData("BER too large", PublicEncodingRules.BER, "048480000000")] + [InlineData("CER too large", PublicEncodingRules.CER, "048480000000")] + [InlineData("DER too large", PublicEncodingRules.DER, "048480000000")] + [InlineData("BER padded too large", PublicEncodingRules.BER, "0486000080000000")] + [InlineData("BER uint.MaxValue", PublicEncodingRules.BER, "0484FFFFFFFF")] + [InlineData("CER uint.MaxValue", PublicEncodingRules.CER, "0484FFFFFFFF")] + [InlineData("DER uint.MaxValue", PublicEncodingRules.DER, "0484FFFFFFFF")] + [InlineData("BER padded uint.MaxValue", PublicEncodingRules.BER, "048800000000FFFFFFFF")] + [InlineData("BER 5 byte spread", PublicEncodingRules.BER, "04850100000000")] + [InlineData("CER 5 byte spread", PublicEncodingRules.CER, "04850100000000")] + [InlineData("DER 5 byte spread", PublicEncodingRules.DER, "04850100000000")] + [InlineData("BER padded 5 byte spread", PublicEncodingRules.BER, "0486000100000000")] + public static void InvalidLengths( + string description, + PublicEncodingRules rules, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)rules); + + Assert.Throws<CryptographicException>(() => reader.ReadTagAndLength(out _, out _)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + public static void IndefiniteLength(PublicEncodingRules ruleSet) + { + // SEQUENCE (indefinite) + // NULL + // End-of-Contents + byte[] data = { 0x30, 0x80, 0x05, 0x00, 0x00, 0x00 }; + AsnReader reader = new AsnReader(data, (AsnEncodingRules)ruleSet); + + Asn1Tag tag = reader.ReadTagAndLength(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"); + } + + [Theory] + [InlineData(0, "0483000000")] + [InlineData(1, "048A00000000000000000001")] + [InlineData(128, "049000000000000000000000000000000080")] + public static void BerNonMinimalLength(int expectedLength, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, AsnEncodingRules.BER); + + Asn1Tag tag = reader.ReadTagAndLength(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"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, 4, 0, 5, "0483000000" + "0500")] + [InlineData(PublicEncodingRules.DER, 1, 1, 2, "0101" + "FF")] + [InlineData(PublicEncodingRules.CER, 0x10, null, 2, "3080" + "0500" + "0000")] + public static void ReadWithDataRemaining( + PublicEncodingRules ruleSet, + int tagValue, + int? expectedLength, + int expectedBytesRead, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Asn1Tag tag = reader.ReadTagAndLength(out int? length, out int bytesRead); + + Assert.Equal(expectedBytesRead, bytesRead); + Assert.Equal(tagValue, tag.TagValue); + Assert.Equal(expectedLength, length); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadNamedBitList.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadNamedBitList.cs new file mode 100644 index 0000000000..687831de4f --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadNamedBitList.cs @@ -0,0 +1,315 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadNamedBitList : Asn1ReaderTests + { + [Flags] + public enum X509KeyUsageCSharpStyle + { + None = 0, + DigitalSignature = 1, + NonRepudiation = 1 << 1, + KeyEncipherment = 1 << 2, + DataEncipherment = 1 << 3, + KeyAgreement = 1 << 4, + KeyCertSign = 1 << 5, + CrlSign = 1 << 6, + EncipherOnly = 1 << 7, + DecipherOnly = 1 << 8, + } + + [Flags] + public enum ULongFlags : ulong + { + None = 0, + Min = 1, + Mid = 1L << 32, + AlmostMax = 1L << 62, + Max = 1UL << 63, + } + + [Flags] + public enum LongFlags : long + { + None = 0, + Mid = 1L << 32, + Max = 1L << 62, + Min = long.MinValue, + } + + [Theory] + [InlineData( + PublicEncodingRules.BER, + typeof(X509KeyUsageCSharpStyle), + X509KeyUsageCSharpStyle.None, + "030100")] + [InlineData( + PublicEncodingRules.CER, + typeof(X509KeyUsageCSharpStyle), + X509KeyUsageCSharpStyle.DecipherOnly | X509KeyUsageCSharpStyle.KeyCertSign, + "0303070480")] + [InlineData( + PublicEncodingRules.DER, + typeof(X509KeyUsageCSharpStyle), + X509KeyUsageCSharpStyle.KeyAgreement, + "03020308")] + [InlineData( + PublicEncodingRules.BER, + typeof(LongFlags), + LongFlags.Mid | LongFlags.Max, + "0309010000000080000002")] + [InlineData( + PublicEncodingRules.CER, + typeof(LongFlags), + LongFlags.Mid | LongFlags.Min, + "0309000000000080000001")] + [InlineData( + PublicEncodingRules.DER, + typeof(LongFlags), + LongFlags.Min | LongFlags.Max, + "0309000000000000000003")] + // BER: Unused bits are unmapped, regardless of value. + [InlineData( + PublicEncodingRules.BER, + typeof(X509KeyUsageCSharpStyle), + X509KeyUsageCSharpStyle.DecipherOnly | X509KeyUsageCSharpStyle.KeyCertSign, + "030307048F")] + // BER: Trailing zeros are permitted. + [InlineData( + PublicEncodingRules.BER, + typeof(X509KeyUsageCSharpStyle), + X509KeyUsageCSharpStyle.DecipherOnly | X509KeyUsageCSharpStyle.KeyCertSign | X509KeyUsageCSharpStyle.DataEncipherment, + "03050014800000")] + // BER: Trailing 0-bits don't have to be declared "unused" + [InlineData( + PublicEncodingRules.BER, + typeof(X509KeyUsageCSharpStyle), + X509KeyUsageCSharpStyle.DecipherOnly | X509KeyUsageCSharpStyle.KeyCertSign | X509KeyUsageCSharpStyle.DataEncipherment, + "0303001480")] + public static void VerifyReadNamedBitListEncodings( + PublicEncodingRules ruleSet, + Type enumType, + long enumValue, + string inputHex) + { + byte[] inputBytes = inputHex.HexToByteArray(); + + AsnReader reader = new AsnReader(inputBytes, (AsnEncodingRules)ruleSet); + Enum readValue = reader.GetNamedBitListValue(enumType); + + Assert.Equal(Enum.ToObject(enumType, enumValue), readValue); + } + + [Theory] + [InlineData( + PublicEncodingRules.BER, + typeof(ULongFlags), + ULongFlags.Mid | ULongFlags.Max, + "0309000000000080000001")] + [InlineData( + PublicEncodingRules.CER, + typeof(ULongFlags), + ULongFlags.Min | ULongFlags.Mid, + "0306078000000080")] + [InlineData( + PublicEncodingRules.DER, + typeof(ULongFlags), + ULongFlags.Min | ULongFlags.Max, + "0309008000000000000001")] + public static void VerifyReadNamedBitListEncodings_ULong( + PublicEncodingRules ruleSet, + Type enumType, + ulong enumValue, + string inputHex) + { + byte[] inputBytes = inputHex.HexToByteArray(); + + AsnReader reader = new AsnReader(inputBytes, (AsnEncodingRules)ruleSet); + Enum readValue = reader.GetNamedBitListValue(enumType); + + Assert.Equal(Enum.ToObject(enumType, enumValue), readValue); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void VerifyGenericReadNamedBitList(PublicEncodingRules ruleSet) + { + string inputHex = "0306078000000080" + "0309010000000080000002"; + AsnReader reader = new AsnReader(inputHex.HexToByteArray(), (AsnEncodingRules)ruleSet); + + ULongFlags uLongFlags = reader.GetNamedBitListValue<ULongFlags>(); + LongFlags longFlags = reader.GetNamedBitListValue<LongFlags>(); + + Assert.False(reader.HasData); + Assert.Equal(ULongFlags.Mid | ULongFlags.Min, uLongFlags); + Assert.Equal(LongFlags.Mid | LongFlags.Max, longFlags); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void ReadNamedBitList_RequiresFlags(PublicEncodingRules ruleSet) + { + string inputHex = "030100"; + AsnReader reader = new AsnReader(inputHex.HexToByteArray(), (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tFlagsEnum", + () => reader.GetNamedBitListValue<AsnEncodingRules>()); + + Assert.True(reader.HasData, "reader.HasData"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void ReadNamedBitList_DataOutOfRange(PublicEncodingRules ruleSet) + { + string inputHex = "0309000000000100000001"; + + AsnReader reader = new AsnReader(inputHex.HexToByteArray(), (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>()); + + Assert.True(reader.HasData, "reader.HasData"); + } + + [Theory] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void ReadNamedBitList_ExcessiveBytes(PublicEncodingRules ruleSet) + { + string inputHex = "03050014800000"; + + AsnReader reader = new AsnReader(inputHex.HexToByteArray(), (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>()); + + Assert.True(reader.HasData, "reader.HasData"); + } + + [Theory] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void ReadNamedBitList_ExcessiveBits(PublicEncodingRules ruleSet) + { + string inputHex = "0303061480"; + + AsnReader reader = new AsnReader(inputHex.HexToByteArray(), (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>()); + + Assert.True(reader.HasData, "reader.HasData"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = { 3, 2, 1, 2 }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>(new Asn1Tag(TagClass.ContextSpecific, 0))); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + Assert.Equal( + X509KeyUsageCSharpStyle.CrlSign, + reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>()); + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x87, 2, 2, 4 }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>()); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>(new Asn1Tag(TagClass.Application, 0))); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>(new Asn1Tag(TagClass.ContextSpecific, 1))); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + Assert.Equal( + X509KeyUsageCSharpStyle.KeyCertSign, + reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>(new Asn1Tag(TagClass.ContextSpecific, 7))); + + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "0303070080", PublicTagClass.Universal, 3)] + [InlineData(PublicEncodingRules.CER, "0303070080", PublicTagClass.Universal, 3)] + [InlineData(PublicEncodingRules.DER, "0303070080", PublicTagClass.Universal, 3)] + [InlineData(PublicEncodingRules.BER, "8003070080", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C03070080", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A4603070080", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Equal( + X509KeyUsageCSharpStyle.DecipherOnly, + reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>( + new Asn1Tag((TagClass)tagClass, tagValue, true))); + + Assert.False(reader.HasData); + + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Equal( + X509KeyUsageCSharpStyle.DecipherOnly, + reader.GetNamedBitListValue<X509KeyUsageCSharpStyle>( + new Asn1Tag((TagClass)tagClass, tagValue, false))); + + Assert.False(reader.HasData); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadNull.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadNull.cs new file mode 100644 index 0000000000..570f3dabd3 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadNull.cs @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadNull : Asn1ReaderTests + { + [Theory] + [InlineData(PublicEncodingRules.BER, "0500")] + [InlineData(PublicEncodingRules.CER, "0500")] + [InlineData(PublicEncodingRules.DER, "0500")] + [InlineData(PublicEncodingRules.BER, "0583000000")] + public static void ReadNull_Success(PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + reader.ReadNull(); + Assert.False(reader.HasData, "reader.HasData"); + } + + [Theory] + [InlineData("Long length", PublicEncodingRules.CER, "0583000000")] + [InlineData("Long length", PublicEncodingRules.DER, "0583000000")] + [InlineData("Constructed definite length", PublicEncodingRules.BER, "2500")] + [InlineData("Constructed definite length", PublicEncodingRules.DER, "2500")] + [InlineData("Constructed indefinite length", PublicEncodingRules.BER, "25800000")] + [InlineData("Constructed indefinite length", PublicEncodingRules.CER, "25800000")] + [InlineData("No length", PublicEncodingRules.BER, "05")] + [InlineData("No length", PublicEncodingRules.CER, "05")] + [InlineData("No length", PublicEncodingRules.DER, "05")] + [InlineData("No data", PublicEncodingRules.BER, "")] + [InlineData("No data", PublicEncodingRules.CER, "")] + [InlineData("No data", PublicEncodingRules.DER, "")] + [InlineData("NonEmpty", PublicEncodingRules.BER, "050100")] + [InlineData("NonEmpty", PublicEncodingRules.CER, "050100")] + [InlineData("NonEmpty", PublicEncodingRules.DER, "050100")] + [InlineData("Incomplete length", PublicEncodingRules.BER, "0581")] + public static void ReadNull_Throws(string description, PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.ReadNull()); + } + + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = { 5, 0 }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.ReadNull(new Asn1Tag(UniversalTagNumber.Integer))); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.ReadNull(new Asn1Tag(TagClass.ContextSpecific, 0))); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + reader.ReadNull(); + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x87, 0 }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.ReadNull(new Asn1Tag(UniversalTagNumber.Integer))); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.ReadNull()); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>(() => reader.ReadNull(new Asn1Tag(TagClass.Application, 0))); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>(() => reader.ReadNull(new Asn1Tag(TagClass.ContextSpecific, 1))); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + reader.ReadNull(new Asn1Tag(TagClass.ContextSpecific, 7)); + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "0500", PublicTagClass.Universal, 5)] + [InlineData(PublicEncodingRules.CER, "0500", PublicTagClass.Universal, 5)] + [InlineData(PublicEncodingRules.DER, "0500", PublicTagClass.Universal, 5)] + [InlineData(PublicEncodingRules.BER, "8000", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C00", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A4600", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + reader.ReadNull(new Asn1Tag((TagClass)tagClass, tagValue, true)); + Assert.False(reader.HasData); + + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + reader.ReadNull(new Asn1Tag((TagClass)tagClass, tagValue, false)); + Assert.False(reader.HasData); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadObjectIdentifier.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadObjectIdentifier.cs new file mode 100644 index 0000000000..92322468e7 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadObjectIdentifier.cs @@ -0,0 +1,286 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using System.Text; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadObjectIdentifier : Asn1ReaderTests + { + [Theory] + [InlineData("Wrong tag", PublicEncodingRules.BER, "010100")] + [InlineData("Wrong tag", PublicEncodingRules.CER, "010100")] + [InlineData("Wrong tag", PublicEncodingRules.DER, "010100")] + [InlineData("Overreaching length", PublicEncodingRules.BER, "0608883703")] + [InlineData("Overreaching length", PublicEncodingRules.CER, "0608883703")] + [InlineData("Overreaching length", PublicEncodingRules.DER, "0608883703")] + [InlineData("Zero length", PublicEncodingRules.BER, "0600")] + [InlineData("Zero length", PublicEncodingRules.CER, "0600")] + [InlineData("Zero length", PublicEncodingRules.DER, "0600")] + [InlineData("Constructed Definite Form", PublicEncodingRules.BER, "2605" + "0603883703")] + [InlineData("Constructed Indefinite Form", PublicEncodingRules.BER, "2680" + "0603883703" + "0000")] + [InlineData("Constructed Indefinite Form", PublicEncodingRules.CER, "2680" + "0603883703" + "0000")] + [InlineData("Unresolved carry-bit (first sub-identifier)", PublicEncodingRules.BER, "060188")] + [InlineData("Unresolved carry-bit (first sub-identifier)", PublicEncodingRules.CER, "060188")] + [InlineData("Unresolved carry-bit (first sub-identifier)", PublicEncodingRules.DER, "060188")] + [InlineData("Unresolved carry-bit (later sub-identifier)", PublicEncodingRules.BER, "0603883781")] + [InlineData("Unresolved carry-bit (later sub-identifier)", PublicEncodingRules.CER, "0603883781")] + [InlineData("Unresolved carry-bit (later sub-identifier)", PublicEncodingRules.DER, "0603883781")] + [InlineData("Sub-Identifier with leading 0x80", PublicEncodingRules.BER, "060488378001")] + [InlineData("Sub-Identifier with leading 0x80", PublicEncodingRules.CER, "060488378001")] + [InlineData("Sub-Identifier with leading 0x80", PublicEncodingRules.DER, "060488378001")] + public static void ReadObjectIdentifier_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.ReadObjectIdentifier(true)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "0603883703", "2.999.3")] + [InlineData(PublicEncodingRules.CER, "06028837", "2.999")] + [InlineData(PublicEncodingRules.DER, "06068837C27B0302", "2.999.8571.3.2")] + [InlineData(PublicEncodingRules.BER, "0603550406", "2.5.4.6")] + [InlineData(PublicEncodingRules.CER, "06092A864886F70D010105", "1.2.840.113549.1.1.5")] + [InlineData(PublicEncodingRules.DER, "060100", "0.0")] + [InlineData(PublicEncodingRules.BER, "06080992268993F22C63", "0.9.2342.19200300.99")] + [InlineData( + PublicEncodingRules.DER, + "0616824F83F09DA7EBCFDEE0C7A1A7B2C0948CC8F9D77603", + // Using the rules of ITU-T-REC-X.667-201210 for 2.25.{UUID} unregistered arcs, and + // their sample value of f81d4fae-7dec-11d0-a765-00a0c91e6bf6 + // this is + // { joint-iso-itu-t(2) uuid(255) thatuuid(329800735698586629295641978511506172918) three(3) } + "2.255.329800735698586629295641978511506172918.3")] + public static void ReadObjectIdentifierAsString_Success( + PublicEncodingRules ruleSet, + string inputHex, + string expectedValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + string oidValue = reader.ReadObjectIdentifierAsString(); + Assert.Equal(expectedValue, oidValue); + } + + [Theory] + // Start at a UUID as a big integer. 128 semantic bits takes 19 + // content bytes to write down. Walk it backwards to 1. + // This uses the OID from the last case of ReadObjectIdentifierAsString_Success, but + // without the "255" arc (therefore the initial second arc is the UUID decimal value - 80) + [InlineData("061383F09DA7EBCFDEE0C7A1A7B2C0948CC8F9D776", "2.329800735698586629295641978511506172838")] + // Drop the last byte, clear the high bit in the last remaining byte, secondArc = (secondArc + 80) >> 7 - 80. + [InlineData("061283F09DA7EBCFDEE0C7A1A7B2C0948CC8F957", "2.2576568247645208041372202957121141895")] + [InlineData("061183F09DA7EBCFDEE0C7A1A7B2C0948CC879", "2.20129439434728187823220335602508841")] + [InlineData("061083F09DA7EBCFDEE0C7A1A7B2C0948C48", "2.157261245583813967368908871894520")] + [InlineData("060F83F09DA7EBCFDEE0C7A1A7B2C0940C", "2.1228603481123546620069600561596")] + [InlineData("060E83F09DA7EBCFDEE0C7A1A7B2C014", "2.9598464696277707969293754308")] + [InlineData("060D83F09DA7EBCFDEE0C7A1A7B240", "2.74988005439669593510107376")] + [InlineData("060C83F09DA7EBCFDEE0C7A1A732", "2.585843792497418699297634")] + [InlineData("060B83F09DA7EBCFDEE0C7A127", "2.4576904628886083588183")] + [InlineData("060A83F09DA7EBCFDEE0C721", "2.35757067413172527953")] + [InlineData("060983F09DA7EBCFDEE047", "2.279352089165410295")] + [InlineData("060883F09DA7EBCFDE60", "2.2182438196604688")] + [InlineData("060783F09DA7EBCF5E", "2.17050298410894")] + [InlineData("060683F09DA7EB4F", "2.133205456255")] + [InlineData("060583F09DA76B", "2.1040667547")] + [InlineData("060483F09D27", "2.8130135")] + [InlineData("060383F01D", "2.63437")] + [InlineData("06028370", "2.416")] + [InlineData("060103", "0.3")] + public static void VerifyMultiByteParsing(string inputHex, string expectedValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, AsnEncodingRules.DER); + + string oidValue = reader.ReadObjectIdentifierAsString(); + Assert.Equal(expectedValue, oidValue); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "06082A864886F70D0307", false, "3des")] + [InlineData(PublicEncodingRules.CER, "06082A864886F70D0307", true, "1.2.840.113549.3.7")] + [InlineData(PublicEncodingRules.DER, "0609608648016503040201", true, "2.16.840.1.101.3.4.2.1")] + [InlineData(PublicEncodingRules.BER, "0609608648016503040201", false, "sha256")] + public static void ReadObjectIdentifier_SkipFriendlyName( + PublicEncodingRules ruleSet, + string inputHex, + bool skipFriendlyName, + string expectedFriendlyName) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Oid oid = reader.ReadObjectIdentifier(skipFriendlyName); + Assert.Equal(expectedFriendlyName, oid.FriendlyName); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = "06028837".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.GetIntegerBytes(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.ReadObjectIdentifierAsString(new Asn1Tag(TagClass.ContextSpecific, 0))); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + Assert.Equal("2.999", reader.ReadObjectIdentifierAsString()); + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = "87028837".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.GetIntegerBytes(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.ReadObjectIdentifierAsString()); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.ReadObjectIdentifierAsString(new Asn1Tag(TagClass.Application, 0))); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.ReadObjectIdentifierAsString(new Asn1Tag(TagClass.ContextSpecific, 1))); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + Assert.Equal( + "2.999", + reader.ReadObjectIdentifierAsString(new Asn1Tag(TagClass.ContextSpecific, 7))); + + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "06028837", PublicTagClass.Universal, 6)] + [InlineData(PublicEncodingRules.CER, "06028837", PublicTagClass.Universal, 6)] + [InlineData(PublicEncodingRules.DER, "06028837", PublicTagClass.Universal, 6)] + [InlineData(PublicEncodingRules.BER, "80028837", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C028837", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A46028837", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + string val1 = reader.ReadObjectIdentifierAsString(new Asn1Tag((TagClass)tagClass, tagValue, true)); + + Assert.False(reader.HasData); + + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + string val2 = reader.ReadObjectIdentifierAsString(new Asn1Tag((TagClass)tagClass, tagValue, false)); + + Assert.False(reader.HasData); + + Assert.Equal(val1, val2); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void ReadVeryLongOid(PublicEncodingRules ruleSet) + { + byte[] inputData = new byte[100000]; + // 06 83 02 00 00 (OBJECT IDENTIFIER, 65536 bytes). + inputData[0] = 0x06; + inputData[1] = 0x83; + inputData[2] = 0x01; + inputData[3] = 0x00; + inputData[4] = 0x00; + // and the rest are all zero. + + // The first byte produces "0.0". Each of the remaining 65535 bytes produce + // another ".0". + const int ExpectedLength = 65536 * 2 + 1; + StringBuilder builder = new StringBuilder(ExpectedLength); + builder.Append('0'); + + for (int i = 0; i <= ushort.MaxValue; i++) + { + builder.Append('.'); + builder.Append(0); + } + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + string oidString = reader.ReadObjectIdentifierAsString(); + + Assert.Equal(ExpectedLength, oidString.Length); + Assert.Equal(builder.ToString(), oidString); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void ReadVeryLongOidArc(PublicEncodingRules ruleSet) + { + byte[] inputData = new byte[255]; + // 06 81 93 (OBJECT IDENTIFIER, 147 bytes). + inputData[0] = 0x06; + inputData[1] = 0x81; + inputData[2] = 0x93; + + // With 147 bytes we get 147*7 = 1029 value bits. + // The smallest legal number to encode would have a top byte of 0x81, + // leaving 1022 bits remaining. If they're all zero then we have 2^1022. + // + // Since it's our first sub-identifier it's really encoding "2.(2^1022 - 80)". + inputData[3] = 0x81; + // Leave the last byte as 0. + new Span<byte>(inputData, 4, 145).Fill(0x80); + + const string ExpectedOid = + "2." + + "449423283715578976932326297697256183404494244735576643183575" + + "202894331689513752407831771193306018840052800284699678483394" + + "146974422036041556232118576598685310944419733562163713190755" + + "549003115235298632707380212514422095376705856157203684782776" + + "352068092908376276711465745599868114846199290762088390824060" + + "56034224"; + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + string oidString = reader.ReadObjectIdentifierAsString(); + Assert.Equal(ExpectedOid, oidString); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadOctetString.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadOctetString.cs new file mode 100644 index 0000000000..4167418b43 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadOctetString.cs @@ -0,0 +1,559 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadOctetString : Asn1ReaderTests + { + [Theory] + [InlineData("Constructed Payload", PublicEncodingRules.BER, "2402040100")] + [InlineData("Constructed Payload-Indefinite", PublicEncodingRules.BER, "248004010000")] + // This value is actually invalid CER, but it returns false since it's not primitive and + // it isn't worth preempting the descent to find out it was invalid. + [InlineData("Constructed Payload-Indefinite", PublicEncodingRules.CER, "248004010000")] + public static void TryGetOctetStringBytes_Fails( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryGetPrimitiveOctetStringBytes(out ReadOnlyMemory<byte> contents); + + Assert.False(didRead, "reader.TryGetOctetStringBytes"); + Assert.Equal(0, contents.Length); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, 0, "0400")] + [InlineData(PublicEncodingRules.BER, 1, "040100")] + [InlineData(PublicEncodingRules.BER, 2, "040201FE")] + [InlineData(PublicEncodingRules.CER, 5, "040502FEEFF00C")] + [InlineData(PublicEncodingRules.DER, 2, "04020780")] + [InlineData(PublicEncodingRules.DER, 5, "040500FEEFF00D" + "0500")] + public static void TryGetOctetStringBytes_Success( + PublicEncodingRules ruleSet, + int expectedLength, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryGetPrimitiveOctetStringBytes(out ReadOnlyMemory<byte> contents); + + Assert.True(didRead, "reader.TryGetOctetStringBytes"); + Assert.Equal(expectedLength, contents.Length); + } + + [Theory] + [InlineData("Wrong Tag", PublicEncodingRules.BER, "0500")] + [InlineData("Wrong Tag", PublicEncodingRules.CER, "0500")] + [InlineData("Wrong Tag", PublicEncodingRules.DER, "0500")] + [InlineData("Bad Length", PublicEncodingRules.BER, "040200")] + [InlineData("Bad Length", PublicEncodingRules.CER, "040200")] + [InlineData("Bad Length", PublicEncodingRules.DER, "040200")] + [InlineData("Constructed Form", PublicEncodingRules.DER, "2403040100")] + public static void TryGetOctetStringBytes_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveOctetStringBytes(out ReadOnlyMemory<byte> contents)); + } + + [Fact] + public static void TryGetOctetStringBytes_Throws_CER_TooLong() + { + // CER says that the maximum encoding length for an OctetString primitive + // is 1000. + // + // So we need 04 [1001] { 1001 0x00s } + // 1001 => 0x3E9, so the length encoding is 82 03 E9. + // 1001 + 3 + 1 == 1005 + byte[] input = new byte[1005]; + input[0] = 0x04; + input[1] = 0x82; + input[2] = 0x03; + input[3] = 0xE9; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveOctetStringBytes(out ReadOnlyMemory<byte> contents)); + } + + [Fact] + public static void TryGetOctetStringBytes_Success_CER_MaxLength() + { + // CER says that the maximum encoding length for an OctetString primitive + // is 1000. + // + // So we need 04 [1000] { 1000 anythings } + // 1000 => 0x3E8, so the length encoding is 82 03 E8. + // 1000 + 3 + 1 == 1004 + byte[] input = new byte[1004]; + input[0] = 0x04; + input[1] = 0x82; + input[2] = 0x03; + input[3] = 0xE8; + + // Contents + input[4] = 0x02; + input[5] = 0xA0; + input[1002] = 0xA5; + input[1003] = 0xFC; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + + bool success = reader.TryGetPrimitiveOctetStringBytes(out ReadOnlyMemory<byte> contents); + + Assert.True(success, "reader.TryGetOctetStringBytes"); + Assert.Equal(1000, contents.Length); + + // Check that it is, in fact, the same memory. No copies with this API. + Assert.True( + Unsafe.AreSame( + ref contents.Span.DangerousGetPinnableReference(), + ref input[4])); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "04020780")] + [InlineData(PublicEncodingRules.BER, "040207FF")] + [InlineData(PublicEncodingRules.CER, "04020780")] + [InlineData(PublicEncodingRules.DER, "04020780")] + [InlineData( + PublicEncodingRules.BER, + "2480" + + "2480" + + "0000" + + "04020000" + + "0000")] + public static void TryCopyOctetStringBytes_Fails(PublicEncodingRules ruleSet, string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryCopyOctetStringBytes( + Span<byte>.Empty, + out int bytesWritten); + + Assert.False(didRead, "reader.TryCopyOctetStringBytes"); + Assert.Equal(0, bytesWritten); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "04020780", "0780")] + [InlineData(PublicEncodingRules.BER, "040207FF", "07FF")] + [InlineData(PublicEncodingRules.CER, "04020780", "0780")] + [InlineData(PublicEncodingRules.DER, "04020680", "0680")] + [InlineData(PublicEncodingRules.BER, "24800000", "")] + [InlineData(PublicEncodingRules.BER, "2400", "")] + [InlineData(PublicEncodingRules.BER, "2400" + "0500", "")] + [InlineData( + PublicEncodingRules.BER, + "2480" + + "2480" + + "0000" + + "04020005" + + "0000", + "0005")] + [InlineData( + PublicEncodingRules.BER, + "2480" + + "2406" + + "0401FA" + + "0401CE" + + "2480" + + "2480" + + "2480" + + "0402F00D" + + "0000" + + "0000" + + "04020001" + + "0000" + + "0403000203" + + "040203FF" + + "2480" + + "0000" + + "0000", + "FACEF00D000100020303FF")] + public static void TryCopyOctetStringBytes_Success( + PublicEncodingRules ruleSet, + string inputHex, + string expectedHex) + { + byte[] inputData = inputHex.HexToByteArray(); + byte[] output = new byte[expectedHex.Length / 2]; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + bool didRead = reader.TryCopyOctetStringBytes( + output, + out int bytesWritten); + + Assert.True(didRead, "reader.TryCopyOctetStringBytes"); + Assert.Equal(expectedHex, output.AsReadOnlySpan().Slice(0, bytesWritten).ByteArrayToHex()); + } + + private static void TryCopyOctetStringBytes_Throws( + PublicEncodingRules ruleSet, + byte[] input) + { + Assert.Throws<CryptographicException>( + () => + { + AsnReader reader = new AsnReader(input, (AsnEncodingRules)ruleSet); + reader.TryCopyOctetStringBytes( + Span<byte>.Empty, + out int bytesWritten); + }); + } + + [Theory] + [InlineData("Wrong Tag", PublicEncodingRules.BER, "0500")] + [InlineData("Wrong Tag", PublicEncodingRules.CER, "0500")] + [InlineData("Wrong Tag", PublicEncodingRules.DER, "0500")] + [InlineData("Bad Length", PublicEncodingRules.BER, "040200")] + [InlineData("Bad Length", PublicEncodingRules.CER, "040200")] + [InlineData("Bad Length", PublicEncodingRules.DER, "040200")] + [InlineData("Constructed Form", PublicEncodingRules.DER, "2403040100")] + [InlineData("Nested context-specific", PublicEncodingRules.BER, "2404800400FACE")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.BER, "2480800400FACE0000")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.CER, "2480800400FACE0000")] + [InlineData("Nested boolean", PublicEncodingRules.BER, "2403010100")] + [InlineData("Nested boolean (indef)", PublicEncodingRules.BER, "24800101000000")] + [InlineData("Nested boolean (indef)", PublicEncodingRules.CER, "24800101000000")] + [InlineData("Nested constructed form", PublicEncodingRules.CER, "2480" + "2480" + "04010" + "000000000")] + [InlineData("No terminator", PublicEncodingRules.BER, "2480" + "04020000" + "")] + [InlineData("No terminator", PublicEncodingRules.CER, "2480" + "04020000" + "")] + [InlineData("No content", PublicEncodingRules.BER, "2480")] + [InlineData("No content", PublicEncodingRules.CER, "2480")] + [InlineData("No nested content", PublicEncodingRules.CER, "24800000")] + [InlineData("Nested value too long", PublicEncodingRules.BER, "2480040A00")] + [InlineData("Nested value too long - constructed", PublicEncodingRules.BER, "2480240A00")] + [InlineData("Nested value too long - simple", PublicEncodingRules.BER, "2403" + "04050000000000")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.BER, "248020000000")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.CER, "248020000000")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.BER, "2480000100")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.CER, "2480000100")] + [InlineData("LongLength EndOfContents", PublicEncodingRules.BER, "2480008100")] + [InlineData("Constructed Payload-TooShort", PublicEncodingRules.CER, "24800401000000")] + public static void TryCopyOctetStringBytes_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + TryCopyOctetStringBytes_Throws(ruleSet, inputData); + } + + [Fact] + public static void TryCopyOctetStringBytes_Throws_CER_NestedTooLong() + { + // CER says that the maximum encoding length for an OctetString primitive + // is 1000. + // + // This test checks it for a primitive contained within a constructed. + // + // So we need 04 [1001] { 1001 0x00s } + // 1001 => 0x3E9, so the length encoding is 82 03 E9. + // 1001 + 3 + 1 == 1005 + // + // Plus a leading 24 80 (indefinite length constructed) + // and a trailing 00 00 (End of contents) + // == 1009 + byte[] input = new byte[1009]; + // CONSTRUCTED OCTET STRING (indefinite) + input[0] = 0x24; + input[1] = 0x80; + // OCTET STRING (1001) + input[2] = 0x04; + input[3] = 0x82; + input[4] = 0x03; + input[5] = 0xE9; + // EOC implicit since the byte[] initializes to zeros + + TryCopyOctetStringBytes_Throws(PublicEncodingRules.CER, input); + } + + [Fact] + public static void TryCopyOctetStringBytes_Throws_CER_NestedTooShortIntermediate() + { + // CER says that the maximum encoding length for an OctetString primitive + // is 1000, and in the constructed form the lengths must be + // [ 1000, 1000, 1000, ..., len%1000 ] + // + // So 1000, 2, 2 is illegal. + // + // 24 80 (indefinite constructed octet string) + // 04 82 03 08 (octet string, 1000 bytes) + // [1000 content bytes] + // 04 02 (octet string, 2 bytes) + // [2 content bytes] + // 04 02 (octet string, 2 bytes) + // [2 content bytes] + // 00 00 (end of contents) + // Looks like 1,016 bytes. + byte[] input = new byte[1016]; + // CONSTRUCTED OCTET STRING (indefinite) + input[0] = 0x23; + input[1] = 0x80; + // OCTET STRING (1000) + input[2] = 0x04; + input[3] = 0x82; + input[4] = 0x03; + input[5] = 0xE8; + // OCTET STRING (2) + input[1006] = 0x04; + input[1007] = 0x02; + // OCTET STRING (2) + input[1010] = 0x04; + input[1011] = 0x02; + // EOC implicit since the byte[] initializes to zeros + + TryCopyOctetStringBytes_Throws(PublicEncodingRules.CER, input); + } + + [Fact] + public static void TryCopyOctetStringBytes_Success_CER_MaxPrimitiveLength() + { + // CER says that the maximum encoding length for an OctetString primitive + // is 1000. + // + // So we need 04 [1000] { 1000 anythings } + // 1000 => 0x3E8, so the length encoding is 82 03 E8. + // 1000 + 3 + 1 == 1004 + byte[] input = new byte[1004]; + input[0] = 0x04; + input[1] = 0x82; + input[2] = 0x03; + input[3] = 0xE8; + + // Content + input[4] = 0x02; + input[5] = 0xA0; + input[1002] = 0xA5; + input[1003] = 0xFC; + + byte[] output = new byte[1000]; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + + bool success = reader.TryCopyOctetStringBytes( + output, + out int bytesWritten); + + Assert.True(success, "reader.TryCopyOctetStringBytes"); + Assert.Equal(1000, bytesWritten); + + Assert.Equal( + input.AsReadOnlySpan().Slice(4).ByteArrayToHex(), + output.ByteArrayToHex()); + } + + [Fact] + public static void TryCopyOctetStringBytes_Success_CER_MinConstructedLength() + { + // CER says that the maximum encoding length for an OctetString primitive + // is 1000, and that a constructed form must be used for values greater + // than 1000 bytes, with segments dividing up for each thousand + // [1000, 1000, ..., len%1000]. + // + // So our smallest constructed form is 1001 bytes, [1000, 1] + // + // 24 80 (indefinite constructed octet string) + // 04 82 03 E9 (primitive octet string, 1000 bytes) + // [1000 content bytes] + // 04 01 (primitive octet string, 1 byte) + // pp + // 00 00 (end of contents, 0 bytes) + // 1011 total. + byte[] input = new byte[1011]; + int offset = 0; + // CONSTRUCTED OCTET STRING (Indefinite) + input[offset++] = 0x24; + input[offset++] = 0x80; + // OCTET STRING (1000) + input[offset++] = 0x04; + input[offset++] = 0x82; + input[offset++] = 0x03; + input[offset++] = 0xE8; + + // Primitive 1: (55 A0 :: A5 FC) (1000) + input[offset++] = 0x55; + input[offset] = 0xA0; + offset += 997; + input[offset++] = 0xA5; + input[offset++] = 0xFC; + + // OCTET STRING (1) + input[offset++] = 0x04; + input[offset++] = 0x01; + + // Primitive 2: One more byte + input[offset] = 0xF7; + + byte[] expected = new byte[1001]; + offset = 0; + expected[offset++] = 0x55; + expected[offset] = 0xA0; + offset += 997; + expected[offset++] = 0xA5; + expected[offset++] = 0xFC; + expected[offset] = 0xF7; + + byte[] output = new byte[1001]; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + + bool success = reader.TryCopyOctetStringBytes( + output, + out int bytesWritten); + + Assert.True(success, "reader.TryCopyOctetStringBytes"); + Assert.Equal(1001, bytesWritten); + + Assert.Equal( + expected.ByteArrayToHex(), + output.ByteArrayToHex()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = { 4, 1, 0x7E }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.TryGetPrimitiveOctetStringBytes(Asn1Tag.Null, out _)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveOctetStringBytes(new Asn1Tag(TagClass.ContextSpecific, 0), out _)); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + Assert.True(reader.TryGetPrimitiveOctetStringBytes(out ReadOnlyMemory<byte> value)); + Assert.Equal("7E", value.ByteArrayToHex()); + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x87, 2, 0, 0x80 }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.TryGetPrimitiveOctetStringBytes(Asn1Tag.Null, out _)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.TryGetPrimitiveOctetStringBytes(out _)); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveOctetStringBytes(new Asn1Tag(TagClass.Application, 0), out _)); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveOctetStringBytes(new Asn1Tag(TagClass.ContextSpecific, 1), out _)); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + Assert.True( + reader.TryGetPrimitiveOctetStringBytes( + new Asn1Tag(TagClass.ContextSpecific, 7), + out ReadOnlyMemory<byte> value)); + + Assert.Equal("0080", value.ByteArrayToHex()); + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "0401FF", PublicTagClass.Universal, 4)] + [InlineData(PublicEncodingRules.CER, "0401FF", PublicTagClass.Universal, 4)] + [InlineData(PublicEncodingRules.DER, "0401FF", PublicTagClass.Universal, 4)] + [InlineData(PublicEncodingRules.BER, "8001FF", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C01FF", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A4601FF", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.True( + reader.TryGetPrimitiveOctetStringBytes( + new Asn1Tag((TagClass)tagClass, tagValue, true), + out ReadOnlyMemory<byte> val1)); + + Assert.False(reader.HasData); + + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.True( + reader.TryGetPrimitiveOctetStringBytes( + new Asn1Tag((TagClass)tagClass, tagValue, false), + out ReadOnlyMemory<byte> val2)); + + Assert.False(reader.HasData); + + Assert.Equal(val1.ByteArrayToHex(), val2.ByteArrayToHex()); + } + + [Fact] + public static void TryCopyOctetStringBytes_ExtremelyNested() + { + byte[] dataBytes = new byte[4 * 16384]; + + // This will build 2^14 nested indefinite length values. + // In the end, none of them contain any content. + // + // For what it's worth, the initial algorithm succeeded at 1061, and StackOverflowed with 1062. + int end = dataBytes.Length / 2; + + // UNIVERSAL OCTET STRING [Constructed] + const byte Tag = 0x20 | (byte)UniversalTagNumber.OctetString; + + for (int i = 0; i < end; i += 2) + { + dataBytes[i] = Tag; + // Indefinite length + dataBytes[i + 1] = 0x80; + } + + AsnReader reader = new AsnReader(dataBytes, AsnEncodingRules.BER); + + int bytesWritten; + + Assert.True(reader.TryCopyOctetStringBytes(Span<byte>.Empty, out bytesWritten)); + Assert.Equal(0, bytesWritten); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadSequence.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadSequence.cs new file mode 100644 index 0000000000..2c22fc89e9 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadSequence.cs @@ -0,0 +1,338 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadSequence : Asn1ReaderTests + { + [Theory] + [InlineData(PublicEncodingRules.BER, "3000", false, -1)] + [InlineData(PublicEncodingRules.BER, "30800000", false, -1)] + [InlineData(PublicEncodingRules.BER, "3083000000", false, -1)] + [InlineData(PublicEncodingRules.CER, "30800000", false, -1)] + [InlineData(PublicEncodingRules.DER, "3000", false, -1)] + [InlineData(PublicEncodingRules.BER, "3000" + "0500", true, -1)] + [InlineData(PublicEncodingRules.BER, "3002" + "0500", false, 5)] + [InlineData(PublicEncodingRules.CER, "3080" + "0500" + "0000", false, 5)] + [InlineData(PublicEncodingRules.CER, "3080" + "010100" + "0000" + "0500", true, 1)] + [InlineData(PublicEncodingRules.DER, "3005" + "0500" + "0101FF", false, 5)] + public static void ReadSequence_Success( + PublicEncodingRules ruleSet, + string inputHex, + bool expectDataRemaining, + int expectedSequenceTagNumber) + { + byte[] inputData = inputHex.HexToByteArray(); + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + AsnReader sequence = reader.ReadSequence(); + + if (expectDataRemaining) + { + Assert.True(reader.HasData, "reader.HasData"); + } + else + { + Assert.False(reader.HasData, "reader.HasData"); + } + + if (expectedSequenceTagNumber < 0) + { + Assert.False(sequence.HasData, "sequence.HasData"); + } + else + { + Assert.True(sequence.HasData, "sequence.HasData"); + + Asn1Tag firstTag = sequence.PeekTag(); + Assert.Equal(expectedSequenceTagNumber, firstTag.TagValue); + } + } + + [Theory] + [InlineData("Empty", PublicEncodingRules.BER, "")] + [InlineData("Empty", PublicEncodingRules.CER, "")] + [InlineData("Empty", PublicEncodingRules.DER, "")] + [InlineData("Incomplete Tag", PublicEncodingRules.BER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.CER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.DER, "1F")] + [InlineData("Missing Length", PublicEncodingRules.BER, "30")] + [InlineData("Missing Length", PublicEncodingRules.CER, "30")] + [InlineData("Missing Length", PublicEncodingRules.DER, "30")] + [InlineData("Primitive Encoding", PublicEncodingRules.BER, "1000")] + [InlineData("Primitive Encoding", PublicEncodingRules.CER, "1000")] + [InlineData("Primitive Encoding", PublicEncodingRules.DER, "1000")] + [InlineData("Definite Length Encoding", PublicEncodingRules.CER, "3000")] + [InlineData("Indefinite Length Encoding", PublicEncodingRules.DER, "3080" + "0000")] + [InlineData("Missing Content", PublicEncodingRules.BER, "3001")] + [InlineData("Missing Content", PublicEncodingRules.DER, "3001")] + [InlineData("Length Out Of Bounds", PublicEncodingRules.BER, "3005" + "010100")] + [InlineData("Length Out Of Bounds", PublicEncodingRules.DER, "3005" + "010100")] + [InlineData("Missing Content - Indefinite", PublicEncodingRules.BER, "3080")] + [InlineData("Missing Content - Indefinite", PublicEncodingRules.CER, "3080")] + [InlineData("Missing EoC", PublicEncodingRules.BER, "3080" + "010100")] + [InlineData("Missing EoC", PublicEncodingRules.CER, "3080" + "010100")] + [InlineData("Missing Outer EoC", PublicEncodingRules.BER, "3080" + "010100" + ("3080" + "0000"))] + [InlineData("Missing Outer EoC", PublicEncodingRules.CER, "3080" + "010100" + ("3080" + "0000"))] + [InlineData("Wrong Tag - Definite", PublicEncodingRules.BER, "3100")] + [InlineData("Wrong Tag - Definite", PublicEncodingRules.DER, "3100")] + [InlineData("Wrong Tag - Indefinite", PublicEncodingRules.BER, "3180" + "0000")] + [InlineData("Wrong Tag - Indefinite", PublicEncodingRules.CER, "3180" + "0000")] + public static void ReadSequence_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.ReadSequence()); + } + + private static void ReadEcPublicKey(AsnEncodingRules ruleSet, byte[] inputData) + { + AsnReader mainReader = new AsnReader(inputData, ruleSet); + + AsnReader spkiReader = mainReader.ReadSequence(); + Assert.False(mainReader.HasData, "mainReader.HasData after reading SPKI"); + + AsnReader algorithmReader = spkiReader.ReadSequence(); + Assert.True(spkiReader.HasData, "spkiReader.HasData after reading algorithm"); + + ReadOnlyMemory<byte> publicKeyValue; + int unusedBitCount; + + if (!spkiReader.TryGetPrimitiveBitStringValue(out unusedBitCount, out publicKeyValue)) + { + // The correct answer is 65 bytes. + for (int i = 10; ; i *= 2) + { + byte[] buf = new byte[i]; + + if (spkiReader.TryCopyBitStringBytes(buf, out unusedBitCount, out int bytesWritten)) + { + publicKeyValue = new ReadOnlyMemory<byte>(buf, 0, bytesWritten); + break; + } + } + } + + Assert.False(spkiReader.HasData, "spkiReader.HasData after reading subjectPublicKey"); + Assert.True(algorithmReader.HasData, "algorithmReader.HasData before reading"); + + Oid algorithmOid = algorithmReader.ReadObjectIdentifier(true); + Assert.True(algorithmReader.HasData, "algorithmReader.HasData after reading first OID"); + + Assert.Equal("1.2.840.10045.2.1", algorithmOid.Value); + + Oid curveOid = algorithmReader.ReadObjectIdentifier(true); + Assert.False(algorithmReader.HasData, "algorithmReader.HasData after reading second OID"); + + Assert.Equal("1.2.840.10045.3.1.7", curveOid.Value); + + const string PublicKeyValue = + "04" + + "2363DD131DA65E899A2E63E9E05E50C830D4994662FFE883DB2B9A767DCCABA2" + + "F07081B5711BE1DEE90DFC8DE17970C2D937A16CD34581F52B8D59C9E9532D13"; + + Assert.Equal(PublicKeyValue, publicKeyValue.ByteArrayToHex()); + Assert.Equal(0, unusedBitCount); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.DER)] + public static void ReadEcPublicKey_DefiniteLength(PublicEncodingRules ruleSet) + { + const string InputHex = + "3059" + + "3013" + + "06072A8648CE3D0201" + + "06082A8648CE3D030107" + + "0342" + + "00" + + "04" + + "2363DD131DA65E899A2E63E9E05E50C830D4994662FFE883DB2B9A767DCCABA2" + + "F07081B5711BE1DEE90DFC8DE17970C2D937A16CD34581F52B8D59C9E9532D13"; + + byte[] inputData = InputHex.HexToByteArray(); + ReadEcPublicKey((AsnEncodingRules)ruleSet, inputData); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + public static void ReadEcPublicKey_IndefiniteLength(PublicEncodingRules ruleSet) + { + const string InputHex = + "3080" + + "3080" + + "06072A8648CE3D0201" + + "06082A8648CE3D030107" + + "0000" + + "0342" + + "00" + + "04" + + "2363DD131DA65E899A2E63E9E05E50C830D4994662FFE883DB2B9A767DCCABA2" + + "F07081B5711BE1DEE90DFC8DE17970C2D937A16CD34581F52B8D59C9E9532D13" + + "0000"; + + byte[] inputData = InputHex.HexToByteArray(); + ReadEcPublicKey((AsnEncodingRules)ruleSet, inputData); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal_Definite(PublicEncodingRules ruleSet) + { + byte[] inputData = "30020500".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.ReadSequence(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0))); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + AsnReader seq = reader.ReadSequence(); + Assert.Equal("0500", seq.GetEncodedValue().ByteArrayToHex()); + + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + public static void TagMustBeCorrect_Universal_Indefinite(PublicEncodingRules ruleSet) + { + byte[] inputData = "308005000000".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.ReadSequence(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 0))); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + AsnReader seq = reader.ReadSequence(); + Assert.Equal("0500", seq.GetEncodedValue().ByteArrayToHex()); + + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom_Definite(PublicEncodingRules ruleSet) + { + byte[] inputData = "A5020500".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.ReadSequence(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.ReadSequence()); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.ReadSequence(new Asn1Tag(TagClass.Application, 5))); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 7))); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + AsnReader seq = reader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 5)); + Assert.Equal("0500", seq.GetEncodedValue().ByteArrayToHex()); + + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + public static void TagMustBeCorrect_Custom_Indefinite(PublicEncodingRules ruleSet) + { + byte[] inputData = "A58005000000".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.ReadSequence(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.ReadSequence()); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.ReadSequence(new Asn1Tag(TagClass.Application, 5))); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 7))); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + AsnReader seq = reader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 5)); + Assert.Equal("0500", seq.GetEncodedValue().ByteArrayToHex()); + + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "30030101FF", PublicTagClass.Universal, 16)] + [InlineData(PublicEncodingRules.BER, "30800101000000", PublicTagClass.Universal, 16)] + [InlineData(PublicEncodingRules.CER, "30800101000000", PublicTagClass.Universal, 16)] + [InlineData(PublicEncodingRules.DER, "30030101FF", PublicTagClass.Universal, 16)] + [InlineData(PublicEncodingRules.BER, "A0030101FF", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.BER, "A1800101000000", PublicTagClass.ContextSpecific, 1)] + [InlineData(PublicEncodingRules.CER, "6C800101000000", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "FF8A46030101FF", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AsnReader val1 = reader.ReadSequence(new Asn1Tag((TagClass)tagClass, tagValue, true)); + + Assert.False(reader.HasData); + + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AsnReader val2 = reader.ReadSequence(new Asn1Tag((TagClass)tagClass, tagValue, false)); + + Assert.False(reader.HasData); + + Assert.Equal(val1.GetEncodedValue().ByteArrayToHex(), val2.GetEncodedValue().ByteArrayToHex()); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadSetOf.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadSetOf.cs new file mode 100644 index 0000000000..369b203492 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadSetOf.cs @@ -0,0 +1,304 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadSetOf : Asn1ReaderTests + { + [Theory] + [InlineData(PublicEncodingRules.BER, "3100", false, -1)] + [InlineData(PublicEncodingRules.BER, "31800000", false, -1)] + [InlineData(PublicEncodingRules.BER, "3183000000", false, -1)] + [InlineData(PublicEncodingRules.CER, "31800000", false, -1)] + [InlineData(PublicEncodingRules.DER, "3100", false, -1)] + [InlineData(PublicEncodingRules.BER, "3100" + "0500", true, -1)] + [InlineData(PublicEncodingRules.BER, "3102" + "0500", false, 5)] + [InlineData(PublicEncodingRules.CER, "3180" + "0500" + "0000", false, 5)] + [InlineData(PublicEncodingRules.CER, "3180" + "010100" + "0000" + "0500", true, 1)] + [InlineData(PublicEncodingRules.CER, "3180" + "010100" + "0101FF" + "0500" + "0000", false, 1)] + [InlineData(PublicEncodingRules.DER, "3105" + "0101FF" + "0500", false, 1)] + public static void ReadSetOf_Success( + PublicEncodingRules ruleSet, + string inputHex, + bool expectDataRemaining, + int expectedSequenceTagNumber) + { + byte[] inputData = inputHex.HexToByteArray(); + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + AsnReader sequence = reader.ReadSetOf(); + + if (expectDataRemaining) + { + Assert.True(reader.HasData, "reader.HasData"); + } + else + { + Assert.False(reader.HasData, "reader.HasData"); + } + + if (expectedSequenceTagNumber < 0) + { + Assert.False(sequence.HasData, "sequence.HasData"); + } + else + { + Assert.True(sequence.HasData, "sequence.HasData"); + + Asn1Tag firstTag = sequence.PeekTag(); + Assert.Equal(expectedSequenceTagNumber, firstTag.TagValue); + } + } + + [Theory] + [InlineData("Empty", PublicEncodingRules.BER, "")] + [InlineData("Empty", PublicEncodingRules.CER, "")] + [InlineData("Empty", PublicEncodingRules.DER, "")] + [InlineData("Incomplete Tag", PublicEncodingRules.BER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.CER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.DER, "1F")] + [InlineData("Missing Length", PublicEncodingRules.BER, "31")] + [InlineData("Missing Length", PublicEncodingRules.CER, "31")] + [InlineData("Missing Length", PublicEncodingRules.DER, "31")] + [InlineData("Primitive Encoding", PublicEncodingRules.BER, "1100")] + [InlineData("Primitive Encoding", PublicEncodingRules.CER, "1100")] + [InlineData("Primitive Encoding", PublicEncodingRules.DER, "1100")] + [InlineData("Definite Length Encoding", PublicEncodingRules.CER, "3100")] + [InlineData("Indefinite Length Encoding", PublicEncodingRules.DER, "3180" + "0000")] + [InlineData("Missing Content", PublicEncodingRules.BER, "3101")] + [InlineData("Missing Content", PublicEncodingRules.DER, "3101")] + [InlineData("Length Out Of Bounds", PublicEncodingRules.BER, "3105" + "010100")] + [InlineData("Length Out Of Bounds", PublicEncodingRules.DER, "3105" + "010100")] + [InlineData("Missing Content - Indefinite", PublicEncodingRules.BER, "3180")] + [InlineData("Missing Content - Indefinite", PublicEncodingRules.CER, "3180")] + [InlineData("Missing EoC", PublicEncodingRules.BER, "3180" + "010100")] + [InlineData("Missing EoC", PublicEncodingRules.CER, "3180" + "010100")] + [InlineData("Missing Outer EoC", PublicEncodingRules.BER, "3180" + "010100" + ("3180" + "0000"))] + [InlineData("Missing Outer EoC", PublicEncodingRules.CER, "3180" + "010100" + ("3180" + "0000"))] + [InlineData("Wrong Tag - Definite", PublicEncodingRules.BER, "3000")] + [InlineData("Wrong Tag - Definite", PublicEncodingRules.DER, "3000")] + [InlineData("Wrong Tag - Indefinite", PublicEncodingRules.BER, "3080" + "0000")] + [InlineData("Wrong Tag - Indefinite", PublicEncodingRules.CER, "3080" + "0000")] + public static void ReadSetOf_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.ReadSetOf()); + } + + [Theory] + // BER can read out of order (indefinite) + [InlineData(PublicEncodingRules.BER, "3180" + "0101FF" + "010100" + "0000", true, 1)] + // BER can read out of order (definite) + [InlineData(PublicEncodingRules.BER, "3106" + "0101FF" + "010100", true, 1)] + // CER will not read out of order + [InlineData(PublicEncodingRules.CER, "3180" + "0500" + "010100" + "0000", false, 1)] + [InlineData(PublicEncodingRules.CER, "3180" + "0101FF" + "010100" + "0000", false, 1)] + // CER is happy in order: + [InlineData(PublicEncodingRules.CER, "3180" + "010100" + "0500" + "0000", true, 5)] + [InlineData(PublicEncodingRules.CER, "3180" + "010100" + "0101FF" + "0500" + "0000", true, 5)] + [InlineData(PublicEncodingRules.CER, "3180" + "010100" + "010100" + "0500" + "0000", true, 5)] + // DER will not read out of order + [InlineData(PublicEncodingRules.DER, "3106" + "0101FF" + "010100", false, 1)] + [InlineData(PublicEncodingRules.DER, "3105" + "0500" + "010100", false, 1)] + // DER is happy in order: + [InlineData(PublicEncodingRules.DER, "3105" + "010100" + "0500", true, 5)] + [InlineData(PublicEncodingRules.DER, "3108" + "010100" + "0101FF" + "0500", true, 5)] + [InlineData(PublicEncodingRules.DER, "3108" + "010100" + "010100" + "0500", true, 5)] + public static void ReadSetOf_DataSorting( + PublicEncodingRules ruleSet, + string inputHex, + bool expectSuccess, + int lastTagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + AsnReader setOf; + + if (expectSuccess) + { + setOf = reader.ReadSetOf(); + } + else + { + AsnReader alsoReader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => alsoReader.ReadSetOf()); + + setOf = reader.ReadSetOf(skipSortOrderValidation: true); + } + + int lastTag = -1; + + while (setOf.HasData) + { + Asn1Tag tag = setOf.PeekTag(); + lastTag = tag.TagValue; + + // Ignore the return, just drain it. + setOf.GetEncodedValue(); + } + + Assert.Equal(lastTagValue, lastTag); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal_Definite(PublicEncodingRules ruleSet) + { + byte[] inputData = "31020500".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.ReadSetOf(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.ReadSetOf(new Asn1Tag(TagClass.ContextSpecific, 0))); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + AsnReader seq = reader.ReadSetOf(); + Assert.Equal("0500", seq.GetEncodedValue().ByteArrayToHex()); + + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + public static void TagMustBeCorrect_Universal_Indefinite(PublicEncodingRules ruleSet) + { + byte[] inputData = "318005000000".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.ReadSetOf(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.ReadSetOf(new Asn1Tag(TagClass.ContextSpecific, 0))); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + AsnReader seq = reader.ReadSetOf(); + Assert.Equal("0500", seq.GetEncodedValue().ByteArrayToHex()); + + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom_Definite(PublicEncodingRules ruleSet) + { + byte[] inputData = "A5020500".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.ReadSetOf(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.ReadSetOf()); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.ReadSetOf(new Asn1Tag(TagClass.Application, 5))); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.ReadSetOf(new Asn1Tag(TagClass.ContextSpecific, 7))); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + AsnReader seq = reader.ReadSetOf(new Asn1Tag(TagClass.ContextSpecific, 5)); + Assert.Equal("0500", seq.GetEncodedValue().ByteArrayToHex()); + + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + public static void TagMustBeCorrect_Custom_Indefinite(PublicEncodingRules ruleSet) + { + byte[] inputData = "A58005000000".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.ReadSetOf(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.ReadSetOf()); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.ReadSetOf(new Asn1Tag(TagClass.Application, 5))); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.ReadSetOf(new Asn1Tag(TagClass.ContextSpecific, 7))); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + AsnReader seq = reader.ReadSetOf(new Asn1Tag(TagClass.ContextSpecific, 5)); + Assert.Equal("0500", seq.GetEncodedValue().ByteArrayToHex()); + + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "31030101FF", PublicTagClass.Universal, 17)] + [InlineData(PublicEncodingRules.BER, "31800101000000", PublicTagClass.Universal, 17)] + [InlineData(PublicEncodingRules.CER, "31800101000000", PublicTagClass.Universal, 17)] + [InlineData(PublicEncodingRules.DER, "31030101FF", PublicTagClass.Universal, 17)] + [InlineData(PublicEncodingRules.BER, "A0030101FF", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.BER, "A1800101000000", PublicTagClass.ContextSpecific, 1)] + [InlineData(PublicEncodingRules.CER, "6C800101000000", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "FF8A46030101FF", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AsnReader val1 = reader.ReadSetOf(new Asn1Tag((TagClass)tagClass, tagValue, true)); + + Assert.False(reader.HasData); + + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AsnReader val2 = reader.ReadSetOf(new Asn1Tag((TagClass)tagClass, tagValue, false)); + + Assert.False(reader.HasData); + + Assert.Equal(val1.GetEncodedValue().ByteArrayToHex(), val2.GetEncodedValue().ByteArrayToHex()); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadUTF8String.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadUTF8String.cs new file mode 100644 index 0000000000..8397bfc654 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadUTF8String.cs @@ -0,0 +1,701 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadUTF8String : Asn1ReaderTests + { + public static IEnumerable<object[]> ValidEncodingData { get; } = + new object[][] + { + new object[] + { + PublicEncodingRules.BER, + "0C0D4A6F686E20512E20536D697468", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.CER, + "0C0D4A6F686E20512E20536D697468", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.DER, + "0C0D4A6F686E20512E20536D697468", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.BER, + "2C80" + "040D4A6F686E20512E20536D697468" + "0000", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.BER, + "2C0F" + "040D4A6F686E20512E20536D697468", + "John Q. Smith", + }, + new object[] + { + PublicEncodingRules.BER, + "0C00", + "", + }, + new object[] + { + PublicEncodingRules.CER, + "0C00", + "", + }, + new object[] + { + PublicEncodingRules.DER, + "0C00", + "", + }, + new object[] + { + PublicEncodingRules.BER, + "2C00", + "", + }, + new object[] + { + PublicEncodingRules.BER, + "2C80" + "0000", + "", + }, + new object[] + { + PublicEncodingRules.BER, + "2C80" + + "2480" + + // "Dr." + "040344722E" + + // " & " + "0403202620" + + // "Mrs." + "04044D72732E" + + "0000" + + // " " + "040120" + + "2480" + + "240C" + + // "Smith" + "0405536D697468" + + // hyphen (U+2010) + "0403E28090" + + "0000" + + // "Jones" + "04054A6F6E6573" + + "2480" + + // " " + "040120" + + "2480" + + // The next three bytes are U+FE60, small ampersand + // Don't know why any system would break this one character + // into three primitives, but it should work. + "0401EF" + + "0401B9" + + "0401A0" + + "0000" + + // " " + "040120" + + // "children" + "04086368696C6472656E" + + "0000" + + "0000", + "Dr. & Mrs. Smith\u2010Jones \uFE60 children", + }, + }; + + [Theory] + [MemberData(nameof(ValidEncodingData))] + public static void GetUTF8String_Success( + PublicEncodingRules ruleSet, + string inputHex, + string expectedValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + string value = reader.GetCharacterString(UniversalTagNumber.UTF8String); + + Assert.Equal(expectedValue, value); + } + + [Theory] + [MemberData(nameof(ValidEncodingData))] + public static void TryCopyUTF8String( + PublicEncodingRules ruleSet, + string inputHex, + string expectedValue) + { + byte[] inputData = inputHex.HexToByteArray(); + char[] output = new char[expectedValue.Length]; + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + bool copied; + int charsWritten; + + if (output.Length > 0) + { + output[0] = 'a'; + + copied = reader.TryCopyCharacterString( + UniversalTagNumber.UTF8String, + output.AsSpan().Slice(0, expectedValue.Length - 1), + out charsWritten); + + Assert.False(copied, "reader.TryCopyUTF8String - too short"); + Assert.Equal(0, charsWritten); + Assert.Equal('a', output[0]); + } + + copied = reader.TryCopyCharacterString( + UniversalTagNumber.UTF8String, + output, + out charsWritten); + + Assert.True(copied, "reader.TryCopyUTF8String"); + + string actualValue = new string(output, 0, charsWritten); + Assert.Equal(expectedValue, actualValue); + } + + [Theory] + [MemberData(nameof(ValidEncodingData))] + public static void TryCopyUTF8StringBytes( + PublicEncodingRules ruleSet, + string inputHex, + string expectedString) + { + byte[] inputData = inputHex.HexToByteArray(); + string expectedHex = Text.Encoding.UTF8.GetBytes(expectedString).ByteArrayToHex(); + byte[] output = new byte[expectedHex.Length / 2]; + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + bool copied; + int bytesWritten; + + if (output.Length > 0) + { + output[0] = 32; + + copied = reader.TryCopyCharacterStringBytes( + UniversalTagNumber.UTF8String, + output.AsSpan().Slice(0, output.Length - 1), + out bytesWritten); + + Assert.False(copied, "reader.TryCopyUTF8StringBytes - too short"); + Assert.Equal(0, bytesWritten); + Assert.Equal(32, output[0]); + } + + copied = reader.TryCopyCharacterStringBytes( + UniversalTagNumber.UTF8String, + output, + out bytesWritten); + + Assert.True(copied, "reader.TryCopyUTF8StringBytes"); + + Assert.Equal( + expectedHex, + new ReadOnlySpan<byte>(output, 0, bytesWritten).ByteArrayToHex()); + + Assert.Equal(output.Length, bytesWritten); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "0C0120", true)] + [InlineData(PublicEncodingRules.BER, "2C80" + "040120" + "0000", false)] + [InlineData(PublicEncodingRules.BER, "2C03" + "040120", false)] + public static void TryGetUTF8StringBytes( + PublicEncodingRules ruleSet, + string inputHex, + bool expectSuccess) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + bool got = reader.TryGetPrimitiveCharacterStringBytes( + UniversalTagNumber.UTF8String, + out ReadOnlyMemory<byte> contents); + + if (expectSuccess) + { + Assert.True(got, "reader.TryGetUTF8StringBytes"); + + Assert.True( + Unsafe.AreSame( + ref contents.Span.DangerousGetPinnableReference(), + ref inputData[2])); + } + else + { + Assert.False(got, "reader.TryGetUTF8StringBytes"); + Assert.True(contents.IsEmpty, "contents.IsEmpty"); + } + } + + [Theory] + [InlineData("Incomplete Tag", PublicEncodingRules.BER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.CER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.DER, "1F")] + [InlineData("Missing Length", PublicEncodingRules.BER, "0C")] + [InlineData("Missing Length", PublicEncodingRules.CER, "0C")] + [InlineData("Missing Length", PublicEncodingRules.DER, "0C")] + [InlineData("Missing Contents", PublicEncodingRules.BER, "0C01")] + [InlineData("Missing Contents", PublicEncodingRules.CER, "0C01")] + [InlineData("Missing Contents", PublicEncodingRules.DER, "0C01")] + [InlineData("Length Too Long", PublicEncodingRules.BER, "0C034869")] + [InlineData("Length Too Long", PublicEncodingRules.CER, "0C034869")] + [InlineData("Length Too Long", PublicEncodingRules.DER, "0C034869")] + [InlineData("Constructed Form", PublicEncodingRules.DER, "2C03040149")] + public static void TryGetUTF8StringBytes_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveCharacterStringBytes(UniversalTagNumber.UTF8String, out _)); + } + + [Theory] + [InlineData("Empty", PublicEncodingRules.BER, "")] + [InlineData("Empty", PublicEncodingRules.CER, "")] + [InlineData("Empty", PublicEncodingRules.DER, "")] + [InlineData("Incomplete Tag", PublicEncodingRules.BER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.CER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.DER, "1F")] + [InlineData("Missing Length", PublicEncodingRules.BER, "0C")] + [InlineData("Missing Length", PublicEncodingRules.CER, "0C")] + [InlineData("Missing Length", PublicEncodingRules.DER, "0C")] + [InlineData("Missing Contents", PublicEncodingRules.BER, "0C01")] + [InlineData("Missing Contents", PublicEncodingRules.CER, "0C01")] + [InlineData("Missing Contents", PublicEncodingRules.DER, "0C01")] + [InlineData("Missing Contents - Constructed", PublicEncodingRules.BER, "2C01")] + [InlineData("Missing Contents - Constructed Indef", PublicEncodingRules.BER, "2C80")] + [InlineData("Missing Contents - Constructed Indef", PublicEncodingRules.CER, "2C80")] + [InlineData("Length Too Long", PublicEncodingRules.BER, "0C034869")] + [InlineData("Length Too Long", PublicEncodingRules.CER, "0C034869")] + [InlineData("Length Too Long", PublicEncodingRules.DER, "0C034869")] + [InlineData("Definite Constructed Form", PublicEncodingRules.CER, "2C03040149")] + [InlineData("Definite Constructed Form", PublicEncodingRules.DER, "2C03040149")] + [InlineData("Indefinite Constructed Form - Short Payload", PublicEncodingRules.CER, "2C800401490000")] + [InlineData("Indefinite Constructed Form", PublicEncodingRules.DER, "2C800401490000")] + [InlineData("No nested content", PublicEncodingRules.CER, "2C800000")] + [InlineData("No EoC", PublicEncodingRules.BER, "2C80" + "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.BER, "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.CER, "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.DER, "04024869")] + [InlineData("Wrong Tag - Constructed", PublicEncodingRules.BER, "240404024869")] + [InlineData("Wrong Tag - Constructed Indef", PublicEncodingRules.BER, "2480" + "04024869" + "0000")] + [InlineData("Wrong Tag - Constructed Indef", PublicEncodingRules.CER, "2480" + "04024869" + "0000")] + [InlineData("Wrong Tag - Constructed", PublicEncodingRules.DER, "240404024869")] + [InlineData("Nested Bad Tag", PublicEncodingRules.BER, "2C04" + "0C024869")] + [InlineData("Nested context-specific", PublicEncodingRules.BER, "2C04800400FACE")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.BER, "2C80800400FACE0000")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.CER, "2C80800400FACE0000")] + [InlineData("Nested Length Too Long", PublicEncodingRules.BER, "2C07" + ("2402" + "0403") + "040149")] + [InlineData("Nested Simple Length Too Long", PublicEncodingRules.BER, "2C03" + "040548656C6C6F")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.BER, "2C8020000000")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.CER, "2C8020000000")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.BER, "2C80000100")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.CER, "2C80000100")] + [InlineData("LongLength EndOfContents", PublicEncodingRules.BER, "2C80008100")] + public static void TryCopyUTF8StringBytes_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + byte[] outputData = new byte[inputData.Length + 1]; + outputData[0] = 252; + + int bytesWritten = -1; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => reader.TryCopyCharacterStringBytes(UniversalTagNumber.UTF8String, outputData, out bytesWritten)); + + Assert.Equal(-1, bytesWritten); + Assert.Equal(252, outputData[0]); + } + + private static void TryCopyUTF8String_Throws(PublicEncodingRules ruleSet, byte[] inputData) + { + char[] outputData = new char[inputData.Length + 1]; + outputData[0] = 'a'; + + int bytesWritten = -1; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => reader.TryCopyCharacterString(UniversalTagNumber.UTF8String, outputData, out bytesWritten)); + + Assert.Equal(-1, bytesWritten); + Assert.Equal('a', outputData[0]); + } + + [Theory] + [InlineData("Bad UTF8 value", PublicEncodingRules.BER, "0C02E280")] + [InlineData("Bad UTF8 value", PublicEncodingRules.CER, "0C02E280")] + [InlineData("Bad UTF8 value", PublicEncodingRules.DER, "0C02E280")] + [InlineData("Wrong Tag", PublicEncodingRules.BER, "04024869")] + public static void GetUTF8String_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => reader.GetCharacterString(UniversalTagNumber.UTF8String)); + } + + [Theory] + [InlineData("Empty", PublicEncodingRules.BER, "")] + [InlineData("Empty", PublicEncodingRules.CER, "")] + [InlineData("Empty", PublicEncodingRules.DER, "")] + [InlineData("Incomplete Tag", PublicEncodingRules.BER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.CER, "1F")] + [InlineData("Incomplete Tag", PublicEncodingRules.DER, "1F")] + [InlineData("Missing Length", PublicEncodingRules.BER, "0C")] + [InlineData("Missing Length", PublicEncodingRules.CER, "0C")] + [InlineData("Missing Length", PublicEncodingRules.DER, "0C")] + [InlineData("Missing Contents", PublicEncodingRules.BER, "0C01")] + [InlineData("Missing Contents", PublicEncodingRules.CER, "0C01")] + [InlineData("Missing Contents", PublicEncodingRules.DER, "0C01")] + [InlineData("Missing Contents - Constructed", PublicEncodingRules.BER, "2C01")] + [InlineData("Missing Contents - Constructed Indef", PublicEncodingRules.BER, "2C80")] + [InlineData("Missing Contents - Constructed Indef", PublicEncodingRules.CER, "2C80")] + [InlineData("Length Too Long", PublicEncodingRules.BER, "0C034869")] + [InlineData("Length Too Long", PublicEncodingRules.CER, "0C034869")] + [InlineData("Length Too Long", PublicEncodingRules.DER, "0C034869")] + [InlineData("Definite Constructed Form", PublicEncodingRules.CER, "2C03040149")] + [InlineData("Definite Constructed Form", PublicEncodingRules.DER, "2C03040149")] + [InlineData("Indefinite Constructed Form - Short Payload", PublicEncodingRules.CER, "2C800401490000")] + [InlineData("Indefinite Constructed Form", PublicEncodingRules.DER, "2C800401490000")] + [InlineData("No nested content", PublicEncodingRules.CER, "2C800000")] + [InlineData("No EoC", PublicEncodingRules.BER, "2C80" + "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.BER, "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.CER, "04024869")] + [InlineData("Wrong Tag - Primitive", PublicEncodingRules.DER, "04024869")] + [InlineData("Wrong Tag - Constructed", PublicEncodingRules.BER, "240404024869")] + [InlineData("Wrong Tag - Constructed Indef", PublicEncodingRules.BER, "2480" + "04024869" + "0000")] + [InlineData("Wrong Tag - Constructed Indef", PublicEncodingRules.CER, "2480" + "04024869" + "0000")] + [InlineData("Wrong Tag - Constructed", PublicEncodingRules.DER, "240404024869")] + [InlineData("Nested Bad Tag", PublicEncodingRules.BER, "2C04" + "0C024869")] + [InlineData("Nested context-specific", PublicEncodingRules.BER, "2C04800400FACE")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.BER, "2C80800400FACE0000")] + [InlineData("Nested context-specific (indef)", PublicEncodingRules.CER, "2C80800400FACE0000")] + [InlineData("Nested Length Too Long", PublicEncodingRules.BER, "2C07" + ("2402" + "0403") + "040149")] + [InlineData("Nested Simple Length Too Long", PublicEncodingRules.BER, "2C03" + "040548656C6C6F")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.BER, "2C8020000000")] + [InlineData("Constructed EndOfContents", PublicEncodingRules.CER, "2C8020000000")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.BER, "2C80000100")] + [InlineData("NonEmpty EndOfContents", PublicEncodingRules.CER, "2C80000100")] + [InlineData("LongLength EndOfContents", PublicEncodingRules.BER, "2C80008100")] + [InlineData("Bad UTF8 value", PublicEncodingRules.BER, "0C02E280")] + public static void TryCopyUTF8String_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + TryCopyUTF8String_Throws(ruleSet, inputData); + } + + [Fact] + public static void TryCopyUTF8String_Throws_CER_NestedTooLong() + { + // CER says that the maximum encoding length for a UTF8String primitive + // is 1000. + // + // This test checks it for a primitive contained within a constructed. + // + // So we need 04 [1001] { 1001 0x00s } + // 1001 => 0x3E9, so the length encoding is 82 03 E9. + // 1001 + 3 + 1 == 1005 + // + // Plus a leading 2C 80 (indefinite length constructed) + // and a trailing 00 00 (End of contents) + // == 1009 + byte[] input = new byte[1009]; + // CONSTRUCTED UTF8 STRING (indefinite) + input[0] = 0x2C; + input[1] = 0x80; + // OCTET STRING (1001) + input[2] = 0x04; + input[3] = 0x82; + input[4] = 0x03; + input[5] = 0xE9; + // EOC implicit since the byte[] initializes to zeros + + TryCopyUTF8String_Throws(PublicEncodingRules.CER, input); + } + + [Fact] + public static void TryCopyUTF8String_Throws_CER_NestedTooShortIntermediate() + { + // CER says that the maximum encoding length for a UTF8String primitive + // is 1000, and in the constructed form the lengths must be + // [ 1000, 1000, 1000, ..., len%1000 ] + // + // So 1000, 2, 2 is illegal. + // + // 2C 80 (indefinite constructed utf8 string) + // 04 82 03 08 (octet string, 1000 bytes) + // [1000 content bytes] + // 04 02 (octet string, 2 bytes) + // [2 content bytes] + // 04 02 (octet string, 2 bytes) + // [2 content bytes] + // 00 00 (end of contents) + // Looks like 1,016 bytes. + byte[] input = new byte[1016]; + // CONSTRUCTED UTF8 STRING (indefinite) + input[0] = 0x2C; + input[1] = 0x80; + // OCTET STRING (1000) + input[2] = 0x03; + input[3] = 0x82; + input[4] = 0x03; + input[5] = 0xE8; + // OCTET STRING (2) + input[1006] = 0x04; + input[1007] = 0x02; + // OCTET STRING (2) + input[1010] = 0x04; + input[1011] = 0x02; + // EOC implicit since the byte[] initializes to zeros + + TryCopyUTF8String_Throws(PublicEncodingRules.CER, input); + } + + [Fact] + public static void TryCopyUTF8StringBytes_Success_CER_MaxPrimitiveLength() + { + // CER says that the maximum encoding length for a UTF8String primitive + // is 1000. + // + // So we need 0C [1000] { 1000 anythings } + // 1000 => 0x3E8, so the length encoding is 82 03 E8. + // 1000 + 3 + 1 == 1004 + byte[] input = new byte[1004]; + input[0] = 0x0C; + input[1] = 0x82; + input[2] = 0x03; + input[3] = 0xE8; + + // Content + input[4] = 0x65; + input[5] = 0x65; + input[1002] = 0x61; + input[1003] = 0x61; + + byte[] output = new byte[1000]; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + + bool success = reader.TryCopyCharacterStringBytes( + UniversalTagNumber.UTF8String, + output, + out int bytesWritten); + + Assert.True(success, "reader.TryCopyUTF8StringBytes"); + Assert.Equal(1000, bytesWritten); + + Assert.Equal( + input.AsReadOnlySpan().Slice(4).ByteArrayToHex(), + output.ByteArrayToHex()); + } + + [Fact] + public static void TryCopyUTF8StringBytes_Success_CER_MinConstructedLength() + { + // CER says that the maximum encoding length for a UTF8String primitive + // is 1000, and that a constructed form must be used for values greater + // than 1000 bytes, with segments dividing up for each thousand + // [1000, 1000, ..., len%1000]. + // + // So our smallest constructed form is 1001 bytes, [1000, 1] + // + // 2C 80 (indefinite constructed utf8 string) + // 04 82 03 E9 (primitive octet string, 1000 bytes) + // [1000 content bytes] + // 04 01 (primitive octet string, 1 byte) + // pp + // 00 00 (end of contents, 0 bytes) + // 1011 total. + byte[] input = new byte[1011]; + int offset = 0; + // CONSTRUCTED UTF8 STRING (Indefinite) + input[offset++] = 0x2C; + input[offset++] = 0x80; + // OCTET STRING (1000) + input[offset++] = 0x04; + input[offset++] = 0x82; + input[offset++] = 0x03; + input[offset++] = 0xE8; + + // Primitive 1: (65 65 :: 61 61) (1000) + input[offset++] = 0x65; + input[offset] = 0x65; + offset += 997; + input[offset++] = 0x61; + input[offset++] = 0x61; + + // OCTET STRING (1) + input[offset++] = 0x04; + input[offset++] = 0x01; + + // Primitive 2: One more byte + input[offset] = 0x2E; + + byte[] expected = new byte[1001]; + offset = 0; + expected[offset++] = 0x65; + expected[offset] = 0x65; + offset += 997; + expected[offset++] = 0x61; + expected[offset++] = 0x61; + expected[offset] = 0x2E; + + byte[] output = new byte[1001]; + + AsnReader reader = new AsnReader(input, AsnEncodingRules.CER); + + bool success = reader.TryCopyCharacterStringBytes( + UniversalTagNumber.UTF8String, + output, + out int bytesWritten); + + Assert.True(success, "reader.TryCopyUTF8StringBytes"); + Assert.Equal(1001, bytesWritten); + + Assert.Equal( + expected.ByteArrayToHex(), + output.ByteArrayToHex()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x0C, 2, (byte)'e', (byte)'l' }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + const UniversalTagNumber EncodingType = UniversalTagNumber.UTF8String; + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.TryGetPrimitiveCharacterStringBytes(Asn1Tag.Null, EncodingType, out _)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveCharacterStringBytes(new Asn1Tag(TagClass.ContextSpecific, 0), EncodingType, out _)); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + Assert.True(reader.TryGetPrimitiveCharacterStringBytes(EncodingType, out ReadOnlyMemory<byte> value)); + Assert.Equal("656C", value.ByteArrayToHex()); + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = { 0x87, 2, (byte)'h', (byte)'i' }; + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + const UniversalTagNumber EncodingType = UniversalTagNumber.UTF8String; + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.TryGetPrimitiveCharacterStringBytes(Asn1Tag.Null, EncodingType, out _)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveCharacterStringBytes(EncodingType, out _)); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveCharacterStringBytes(new Asn1Tag(TagClass.Application, 0), EncodingType, out _)); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.TryGetPrimitiveCharacterStringBytes(new Asn1Tag(TagClass.ContextSpecific, 1), EncodingType, out _)); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + Assert.True( + reader.TryGetPrimitiveCharacterStringBytes( + new Asn1Tag(TagClass.ContextSpecific, 7), + EncodingType, + out ReadOnlyMemory<byte> value)); + + Assert.Equal("6869", value.ByteArrayToHex()); + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "0C026869", PublicTagClass.Universal, 12)] + [InlineData(PublicEncodingRules.CER, "0C026869", PublicTagClass.Universal, 12)] + [InlineData(PublicEncodingRules.DER, "0C026869", PublicTagClass.Universal, 12)] + [InlineData(PublicEncodingRules.BER, "80023132", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C023132", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A46023132", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.True( + reader.TryGetPrimitiveCharacterStringBytes( + new Asn1Tag((TagClass)tagClass, tagValue, true), + UniversalTagNumber.UTF8String, + out ReadOnlyMemory<byte> val1)); + + Assert.False(reader.HasData); + + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.True( + reader.TryGetPrimitiveCharacterStringBytes( + new Asn1Tag((TagClass)tagClass, tagValue, false), + UniversalTagNumber.UTF8String, + out ReadOnlyMemory<byte> val2)); + + Assert.False(reader.HasData); + + Assert.Equal(val1.ByteArrayToHex(), val2.ByteArrayToHex()); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadUtcTime.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadUtcTime.cs new file mode 100644 index 0000000000..290c7b13fa --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReadUtcTime.cs @@ -0,0 +1,238 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public sealed class ReadUtcTime : Asn1ReaderTests + { + [Theory] + // A, B2, C2 + [InlineData(PublicEncodingRules.BER, "17113137303930383130333530332D30373030", 2017, 9, 8, 10, 35, 3, -7, 0)] + [InlineData(PublicEncodingRules.BER, "17113137303930383130333530332D30303530", 2017, 9, 8, 10, 35, 3, 0, -50)] + [InlineData(PublicEncodingRules.BER, "17113137303930383130333530332B30373030", 2017, 9, 8, 10, 35, 3, 7, 0)] + [InlineData(PublicEncodingRules.BER, "17113030303130313030303030302B30303030", 2000, 1, 1, 0, 0, 0, 0, 0)] + [InlineData(PublicEncodingRules.BER, "17113030303130313030303030302D31343030", 2000, 1, 1, 0, 0, 0, -14, 0)] + // A, B2, C1 (only legal form for CER or DER) + [InlineData(PublicEncodingRules.BER, "170D3132303130323233353935395A", 2012, 1, 2, 23, 59, 59, 0, 0)] + [InlineData(PublicEncodingRules.CER, "170D3439313233313233353935395A", 2049, 12, 31, 23, 59, 59, 0, 0)] + [InlineData(PublicEncodingRules.DER, "170D3530303130323132333435365A", 1950, 1, 2, 12, 34, 56, 0, 0)] + // A, B1, C2 + [InlineData(PublicEncodingRules.BER, "170F313730393038313033352D30373030", 2017, 9, 8, 10, 35, 0, -7, 0)] + [InlineData(PublicEncodingRules.BER, "170F323730393038313033352B30393132", 2027, 9, 8, 10, 35, 0, 9, 12)] + // A, B1, C1 + [InlineData(PublicEncodingRules.BER, "170B313230313032323335395A", 2012, 1, 2, 23, 59, 0, 0, 0)] + [InlineData(PublicEncodingRules.BER, "170B343931323331323335395A", 2049, 12, 31, 23, 59, 0, 0, 0)] + // BER Constructed form + [InlineData( + PublicEncodingRules.BER, + "3780" + + "04023132" + + "04023031" + + "2480" + "040130" + "040132" + "0000" + + "040432333539" + + "04830000015A" + + "0000", + 2012, 1, 2, 23, 59, 0, 0, 0)] + public static void ParseTime_Valid( + PublicEncodingRules ruleSet, + string inputHex, + int year, + int month, + int day, + int hour, + int minute, + int second, + int offsetHour, + int offsetMinute) + { + byte[] inputData = inputHex.HexToByteArray(); + + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + DateTimeOffset value = reader.GetUtcTime(); + + Assert.Equal(year, value.Year); + Assert.Equal(month, value.Month); + Assert.Equal(day, value.Day); + Assert.Equal(hour, value.Hour); + Assert.Equal(minute, value.Minute); + Assert.Equal(second, value.Second); + Assert.Equal(0, value.Millisecond); + Assert.Equal(new TimeSpan(offsetHour, offsetMinute, 0), value.Offset); + } + + [Fact] + public static void ParseTime_InvalidValue_LegalString() + { + byte[] inputData = "17113030303030303030303030302D31353030".HexToByteArray(); + + var exception = Assert.Throws<CryptographicException>( + () => + { + AsnReader reader = new AsnReader(inputData, AsnEncodingRules.BER); + reader.GetUtcTime(); + }); + + Assert.NotNull(exception.InnerException); + Assert.IsType<ArgumentOutOfRangeException>(exception.InnerException); + } + + [Theory] + [InlineData(2011, 1912)] + [InlineData(2012, 2012)] + [InlineData(2013, 2012)] + [InlineData(2111, 2012)] + [InlineData(2112, 2112)] + [InlineData(2113, 2112)] + [InlineData(12, 12)] + [InlineData(99, 12)] + [InlineData(111, 12)] + public static void ReadUtcTime_TwoYearMaximum(int maximum, int interpretedYear) + { + byte[] inputData = "170D3132303130323233353935395A".HexToByteArray(); + + AsnReader reader = new AsnReader(inputData, AsnEncodingRules.BER); + DateTimeOffset value = reader.GetUtcTime(maximum); + + Assert.Equal(interpretedYear, value.Year); + } + + [Theory] + [InlineData("A,B2,C2", PublicEncodingRules.CER, "17113137303930383130333530332D30373030")] + [InlineData("A,B2,C2", PublicEncodingRules.DER, "17113137303930383130333530332D30373030")] + [InlineData("A,B1,C2", PublicEncodingRules.CER, "170F313730393038313033352D30373030")] + [InlineData("A,B1,C2", PublicEncodingRules.DER, "170F313730393038313033352D30373030")] + [InlineData("A,B1,C1", PublicEncodingRules.CER, "170B313230313032323335395A")] + [InlineData("A,B1,C1", PublicEncodingRules.DER, "170B313230313032323335395A")] + [InlineData("A,B1,C1-NotZ", PublicEncodingRules.BER, "170B313230313032323335392B")] + [InlineData("A,B1,C2-NotPlusMinus", PublicEncodingRules.BER, "170F313730393038313033352C30373030")] + [InlineData("A,B2,C2-NotPlusMinus", PublicEncodingRules.BER, "17113137303930383130333530332C30373030")] + [InlineData("A,B2,C2-MinuteOutOfRange", PublicEncodingRules.BER, "17113030303030303030303030302D31353630")] + [InlineData("A,B1,C2-MinuteOutOfRange", PublicEncodingRules.BER, "170F303030303030303030302D31353630")] + [InlineData("A1,B2,C1-NotZ", PublicEncodingRules.DER, "170D3530303130323132333435365B")] + [InlineData("A,B2,C2-MissingDigit", PublicEncodingRules.BER, "17103137303930383130333530332C303730")] + [InlineData("A,B2,C2-TooLong", PublicEncodingRules.BER, "17123137303930383130333530332B3037303030")] + [InlineData("WrongTag", PublicEncodingRules.BER, "1A0D3132303130323233353935395A")] + public static void ReadUtcTime_Throws( + string description, + PublicEncodingRules ruleSet, + string inputHex) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>(() => reader.GetUtcTime()); + } + + [Fact] + public static void ReadUtcTime_WayTooBig_Throws() + { + // Need to exceed the length that the shared pool will return for 17: + byte[] inputData = new byte[4097+4]; + inputData[0] = 0x17; + inputData[1] = 0x82; + inputData[2] = 0x10; + inputData[3] = 0x01; + + AsnReader reader = new AsnReader(inputData, AsnEncodingRules.BER); + + Assert.Throws<CryptographicException>(() => reader.GetUtcTime()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Universal(PublicEncodingRules ruleSet) + { + byte[] inputData = "170D3530303130323132333435365A".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.GetUtcTime(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>( + () => reader.GetUtcTime(new Asn1Tag(TagClass.ContextSpecific, 0))); + + Assert.True(reader.HasData, "HasData after wrong tag"); + + Assert.Equal( + new DateTimeOffset(1950, 1, 2, 12, 34, 56, TimeSpan.Zero), + reader.GetUtcTime()); + + Assert.False(reader.HasData, "HasData after read"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void TagMustBeCorrect_Custom(PublicEncodingRules ruleSet) + { + byte[] inputData = "850D3530303130323132333435365A".HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "expectedTag", + () => reader.GetUtcTime(Asn1Tag.Null)); + + Assert.True(reader.HasData, "HasData after bad universal tag"); + + Assert.Throws<CryptographicException>(() => reader.GetUtcTime()); + + Assert.True(reader.HasData, "HasData after default tag"); + + Assert.Throws<CryptographicException>( + () => reader.GetUtcTime(new Asn1Tag(TagClass.Application, 5))); + + Assert.True(reader.HasData, "HasData after wrong custom class"); + + Assert.Throws<CryptographicException>( + () => reader.GetUtcTime(new Asn1Tag(TagClass.ContextSpecific, 7))); + + Assert.True(reader.HasData, "HasData after wrong custom tag value"); + + Assert.Equal( + new DateTimeOffset(1950, 1, 2, 12, 34, 56, TimeSpan.Zero), + reader.GetUtcTime(new Asn1Tag(TagClass.ContextSpecific, 5))); + + Assert.False(reader.HasData, "HasData after reading value"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "170D3530303130323132333435365A", PublicTagClass.Universal, 23)] + [InlineData(PublicEncodingRules.CER, "170D3530303130323132333435365A", PublicTagClass.Universal, 23)] + [InlineData(PublicEncodingRules.DER, "170D3530303130323132333435365A", PublicTagClass.Universal, 23)] + [InlineData(PublicEncodingRules.BER, "800D3530303130323132333435365A", PublicTagClass.ContextSpecific, 0)] + [InlineData(PublicEncodingRules.CER, "4C0D3530303130323132333435365A", PublicTagClass.Application, 12)] + [InlineData(PublicEncodingRules.DER, "DF8A460D3530303130323132333435365A", PublicTagClass.Private, 1350)] + public static void ExpectedTag_IgnoresConstructed( + PublicEncodingRules ruleSet, + string inputHex, + PublicTagClass tagClass, + int tagValue) + { + byte[] inputData = inputHex.HexToByteArray(); + AsnReader reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + DateTimeOffset val1 = reader.GetUtcTime(new Asn1Tag((TagClass)tagClass, tagValue, true)); + + Assert.False(reader.HasData); + + reader = new AsnReader(inputData, (AsnEncodingRules)ruleSet); + + DateTimeOffset val2 = reader.GetUtcTime(new Asn1Tag((TagClass)tagClass, tagValue, false)); + + Assert.False(reader.HasData); + + Assert.Equal(val1, val2); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReaderStateTests.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReaderStateTests.cs new file mode 100644 index 0000000000..2cbfb57018 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Reader/ReaderStateTests.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public static class ReaderStateTests + { + [Fact] + public static void HasDataAndThrowIfNotEmpty() + { + AsnReader reader = new AsnReader(new byte[] { 0x01, 0x01, 0x00 }, AsnEncodingRules.BER); + Assert.True(reader.HasData); + Assert.Throws<CryptographicException>(() => reader.ThrowIfNotEmpty()); + + // Consume the current value and move on. + reader.GetEncodedValue(); + + Assert.False(reader.HasData); + // Assert.NoThrow + reader.ThrowIfNotEmpty(); + } + + [Fact] + public static void HasDataAndThrowIfNotEmpty_StartsEmpty() + { + AsnReader reader = new AsnReader(ReadOnlyMemory<byte>.Empty, AsnEncodingRules.BER); + Assert.False(reader.HasData); + // Assert.NoThrow + reader.ThrowIfNotEmpty(); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/Asn1WriterTests.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/Asn1WriterTests.cs new file mode 100644 index 0000000000..316eb28b59 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/Asn1WriterTests.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public abstract partial class Asn1WriterTests : Asn1ReaderTests + { + internal static void Verify(AsnWriter writer, string expectedHex) + { + byte[] encoded = writer.Encode(); + Assert.Equal(expectedHex, encoded.ByteArrayToHex()); + + // Now verify TryEncode's boundary conditions. + byte[] encoded2 = new byte[encoded.Length + 3]; + encoded2[0] = 255; + encoded2[encoded.Length] = 254; + + Span<byte> dest = encoded2.AsSpan().Slice(0, encoded.Length - 1); + Assert.False(writer.TryEncode(dest, out int bytesWritten), "writer.TryEncode (too small)"); + Assert.Equal(0, bytesWritten); + Assert.Equal(255, encoded2[0]); + Assert.Equal(254, encoded2[encoded.Length]); + + dest = encoded2.AsSpan().Slice(0, encoded.Length); + Assert.True(writer.TryEncode(dest, out bytesWritten), "writer.TryEncode (exact length)"); + Assert.Equal(encoded.Length, bytesWritten); + Assert.True(dest.SequenceEqual(encoded), "dest.SequenceEqual(encoded2) (exact length)"); + Assert.Equal(254, encoded2[encoded.Length]); + + // Start marker was obliterated, but the stop marker is still intact. Keep it there. + Array.Clear(encoded2, 0, bytesWritten); + + dest = encoded2.AsSpan(); + Assert.True(writer.TryEncode(dest, out bytesWritten), "writer.TryEncode (overly big)"); + Assert.Equal(encoded.Length, bytesWritten); + Assert.True(dest.Slice(0, bytesWritten).SequenceEqual(encoded), "dest.SequenceEqual(encoded2) (overly big)"); + Assert.Equal(254, encoded2[encoded.Length]); + } + + internal static unsafe string Stringify(Asn1Tag tag) + { + byte* stackspace = stackalloc byte[10]; + Span<byte> dest = new Span<byte>(stackspace, 10); + + Assert.True(tag.TryWrite(dest, out int size)); + return dest.Slice(0, size).ByteArrayToHex(); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/ComprehensiveWriteTest.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/ComprehensiveWriteTest.cs new file mode 100644 index 0000000000..626d7f59ae --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/ComprehensiveWriteTest.cs @@ -0,0 +1,272 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +using X509KeyUsageCSharpStyle = System.Security.Cryptography.Tests.Asn1.ReadNamedBitList.X509KeyUsageCSharpStyle; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public static class ComprehensiveWriteTest + { + [Fact] + public static void WriteMicrosoftDotComCert() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + // Certificate + writer.PushSequence(); + + // tbsCertificate + writer.PushSequence(); + + // version ([0] EXPLICIT INTEGER) + Asn1Tag context0 = new Asn1Tag(TagClass.ContextSpecific, 0, true); + writer.PushSequence(context0); + writer.WriteInteger(2); + writer.PopSequence(context0); + + BigInteger serialValue = BigInteger.Parse("82365655871428336739211871484630851433"); + writer.WriteInteger(serialValue); + + // signature (algorithm) + writer.PushSequence(); + writer.WriteObjectIdentifier("1.2.840.113549.1.1.11"); + writer.WriteNull(); + writer.PopSequence(); + + // issuer + writer.PushSequence(); + WriteRdn(writer, "2.5.4.6", "US", UniversalTagNumber.PrintableString); + WriteRdn(writer, "2.5.4.10", "Symantec Corporation", UniversalTagNumber.PrintableString); + WriteRdn(writer, "2.5.4.11", "Symantec Trust Network", UniversalTagNumber.PrintableString); + WriteRdn(writer, "2.5.4.3", "Symantec Class 3 EV SSL CA - G3", UniversalTagNumber.PrintableString); + writer.PopSequence(); + + // validity + writer.PushSequence(); + writer.WriteUtcTime(new DateTimeOffset(2014, 10, 15, 0, 0, 0, TimeSpan.Zero)); + writer.WriteUtcTime(new DateTimeOffset(2016, 10, 15, 23, 59, 59, TimeSpan.Zero)); + writer.PopSequence(); + + // subject + writer.PushSequence(); + WriteRdn(writer, "1.3.6.1.4.1.311.60.2.1.3", "US", UniversalTagNumber.PrintableString); + WriteRdn(writer, "1.3.6.1.4.1.311.60.2.1.2", "Washington", UniversalTagNumber.UTF8String); + WriteRdn(writer, "2.5.4.15", "Private Organization", UniversalTagNumber.PrintableString); + WriteRdn(writer, "2.5.4.5", "600413485", UniversalTagNumber.PrintableString); + WriteRdn(writer, "2.5.4.6", "US", UniversalTagNumber.PrintableString); + WriteRdn(writer, "2.5.4.17", "98052", UniversalTagNumber.UTF8String); + WriteRdn(writer, "2.5.4.8", "Washington", UniversalTagNumber.UTF8String); + WriteRdn(writer, "2.5.4.7", "Redmond", UniversalTagNumber.UTF8String); + WriteRdn(writer, "2.5.4.9", "1 Microsoft Way", UniversalTagNumber.UTF8String); + WriteRdn(writer, "2.5.4.10", "Microsoft Corporation", UniversalTagNumber.UTF8String); + WriteRdn(writer, "2.5.4.11", "MSCOM", UniversalTagNumber.UTF8String); + WriteRdn(writer, "2.5.4.3", "www.microsoft.com", UniversalTagNumber.UTF8String); + writer.PopSequence(); + + // subjectPublicKeyInfo + writer.PushSequence(); + // subjectPublicKeyInfo.algorithm + writer.PushSequence(); + writer.WriteObjectIdentifier("1.2.840.113549.1.1.1"); + writer.WriteNull(); + writer.PopSequence(); + + AsnWriter publicKeyWriter = new AsnWriter(AsnEncodingRules.DER); + publicKeyWriter.PushSequence(); + BigInteger modulus = BigInteger.Parse( + "207545550571844404676608632512851454930111394466749205318948660756381" + + "523214360115124048083611193260260272384440199925180817531535965931647" + + "037093368608713442955529617501657176146245891571745113402870077189045" + + "116705181899983704226178882882602868159586789723579670915035003754974" + + "985730226756711782751711104985926458681071638525996766798322809764200" + + "941677343791419428587801897366593842552727222686457866144928124161967" + + "521735393182823375650694786333059783380738262856873316471830589717911" + + "537307419734834201104082715701367336140572971505716740825623220507359" + + "42929758463490933054115079473593821332264673455059897928082590541"); + publicKeyWriter.WriteInteger(modulus); + publicKeyWriter.WriteInteger(65537); + publicKeyWriter.PopSequence(); + + // subjectPublicKeyInfo.subjectPublicKey + writer.WriteBitString(publicKeyWriter.Encode()); + writer.PopSequence(); + + // extensions ([3] EXPLICIT Extensions) + Asn1Tag context3 = new Asn1Tag(TagClass.ContextSpecific, 3); + writer.PushSequence(context3); + writer.PushSequence(); + + Asn1Tag dnsName = new Asn1Tag(TagClass.ContextSpecific, 2); + AsnWriter extensionValueWriter = new AsnWriter(AsnEncodingRules.DER); + extensionValueWriter.PushSequence(); + extensionValueWriter.WriteCharacterString(dnsName, UniversalTagNumber.IA5String, "www.microsoft.com"); + extensionValueWriter.WriteCharacterString(dnsName, UniversalTagNumber.IA5String, "wwwqa.microsoft.com"); + extensionValueWriter.PopSequence(); + + writer.PushSequence(); + writer.WriteObjectIdentifier("2.5.29.17"); + writer.WriteOctetString(extensionValueWriter.Encode()); + writer.PopSequence(); + + writer.PushSequence(); + writer.WriteObjectIdentifier("2.5.29.19"); + // Empty sequence as the payload for a non-CA basic constraint. + writer.WriteOctetString(new byte[] { 0x30, 0x00 }); + writer.PopSequence(); + + extensionValueWriter = new AsnWriter(AsnEncodingRules.DER); + // This extension doesn't use a sequence at all, just Named Bit List. + extensionValueWriter.WriteNamedBitList( + X509KeyUsageCSharpStyle.DigitalSignature | X509KeyUsageCSharpStyle.KeyEncipherment); + + writer.PushSequence(); + writer.WriteObjectIdentifier("2.5.29.15"); + // critical: true + writer.WriteBoolean(true); + writer.WriteOctetString(extensionValueWriter.Encode()); + writer.PopSequence(); + + extensionValueWriter = new AsnWriter(AsnEncodingRules.DER); + extensionValueWriter.PushSequence(); + extensionValueWriter.WriteObjectIdentifier("1.3.6.1.5.5.7.3.1"); + extensionValueWriter.WriteObjectIdentifier("1.3.6.1.5.5.7.3.2"); + extensionValueWriter.PopSequence(); + + writer.PushSequence(); + writer.WriteObjectIdentifier("2.5.29.37"); + writer.WriteOctetString(extensionValueWriter.Encode()); + writer.PopSequence(); + + extensionValueWriter = new AsnWriter(AsnEncodingRules.DER); + + extensionValueWriter.PushSequence(); + extensionValueWriter.PushSequence(); + extensionValueWriter.WriteObjectIdentifier("2.16.840.1.113733.1.7.23.6"); + extensionValueWriter.PushSequence(); + extensionValueWriter.PushSequence(); + extensionValueWriter.WriteObjectIdentifier("1.3.6.1.5.5.7.2.1"); + extensionValueWriter.WriteCharacterString(UniversalTagNumber.IA5String, "https://d.symcb.com/cps"); + extensionValueWriter.PopSequence(); + extensionValueWriter.PushSequence(); + extensionValueWriter.WriteObjectIdentifier("1.3.6.1.5.5.7.2.2"); + extensionValueWriter.PushSequence(); + extensionValueWriter.WriteCharacterString(UniversalTagNumber.VisibleString, "https://d.symcb.com/rpa"); + extensionValueWriter.PopSequence(); + extensionValueWriter.PopSequence(); + extensionValueWriter.PopSequence(); + extensionValueWriter.PopSequence(); + extensionValueWriter.PopSequence(); + + writer.PushSequence(); + writer.WriteObjectIdentifier("2.5.29.32"); + writer.WriteOctetString(extensionValueWriter.Encode()); + writer.PopSequence(); + + byte[] authorityKeyIdentifier = "0159ABE7DD3A0B59A66463D6CF200757D591E76A".HexToByteArray(); + Asn1Tag keyIdentifier = context0; + extensionValueWriter = new AsnWriter(AsnEncodingRules.DER); + extensionValueWriter.PushSequence(); + extensionValueWriter.WriteOctetString(keyIdentifier, authorityKeyIdentifier); + extensionValueWriter.PopSequence(); + + writer.PushSequence(); + writer.WriteObjectIdentifier("2.5.29.35"); + writer.WriteOctetString(extensionValueWriter.Encode()); + writer.PopSequence(); + + Asn1Tag distributionPointChoice = context0; + Asn1Tag fullNameChoice = context0; + Asn1Tag generalNameUriChoice = new Asn1Tag(TagClass.ContextSpecific, 6); + extensionValueWriter = new AsnWriter(AsnEncodingRules.DER); + extensionValueWriter.PushSequence(); + extensionValueWriter.PushSequence(); + extensionValueWriter.PushSequence(distributionPointChoice); + extensionValueWriter.PushSequence(fullNameChoice); + extensionValueWriter.WriteCharacterString( + generalNameUriChoice, + UniversalTagNumber.IA5String, + "http://sr.symcb.com/sr.crl"); + extensionValueWriter.PopSequence(fullNameChoice); + extensionValueWriter.PopSequence(distributionPointChoice); + extensionValueWriter.PopSequence(); + extensionValueWriter.PopSequence(); + + writer.PushSequence(); + writer.WriteObjectIdentifier("2.5.29.31"); + writer.WriteOctetString(extensionValueWriter.Encode()); + writer.PopSequence(); + + extensionValueWriter = new AsnWriter(AsnEncodingRules.DER); + extensionValueWriter.PushSequence(); + extensionValueWriter.PushSequence(); + extensionValueWriter.WriteObjectIdentifier("1.3.6.1.5.5.7.48.1"); + extensionValueWriter.WriteCharacterString( + generalNameUriChoice, + UniversalTagNumber.IA5String, + "http://sr.symcd.com"); + extensionValueWriter.PopSequence(); + extensionValueWriter.PushSequence(); + extensionValueWriter.WriteObjectIdentifier("1.3.6.1.5.5.7.48.2"); + extensionValueWriter.WriteCharacterString( + generalNameUriChoice, + UniversalTagNumber.IA5String, + "http://sr.symcb.com/sr.crt"); + extensionValueWriter.PopSequence(); + extensionValueWriter.PopSequence(); + + writer.PushSequence(); + writer.WriteObjectIdentifier("1.3.6.1.5.5.7.1.1"); + writer.WriteOctetString(extensionValueWriter.Encode()); + writer.PopSequence(); + + writer.PopSequence(); + writer.PopSequence(context3); + + // tbsCertificate + writer.PopSequence(); + + // signatureAlgorithm + writer.PushSequence(); + writer.WriteObjectIdentifier("1.2.840.113549.1.1.11"); + writer.WriteNull(); + writer.PopSequence(); + + // signature + byte[] containsSignature = ( + "010203040506070809" + + "15F8505B627ED7F9F96707097E93A51E7A7E05A3D420A5C258EC7A1CFE1843EC" + + "20ACF728AAFA7A1A1BC222A7CDBF4AF90AA26DEEB3909C0B3FB5C78070DAE3D6" + + "45BFCF840A4A3FDD988C7B3308BFE4EB3FD66C45641E96CA3352DBE2AEB4488A" + + "64A9C5FB96932BA70059CE92BD278B41299FD213471BD8165F924285AE3ECD66" + + "6C703885DCA65D24DA66D3AFAE39968521995A4C398C7DF38DFA82A20372F13D" + + "4A56ADB21B5822549918015647B5F8AC131CC5EB24534D172BC60218A88B65BC" + + "F71C7F388CE3E0EF697B4203720483BB5794455B597D80D48CD3A1D73CBBC609" + + "C058767D1FF060A609D7E3D4317079AF0CD0A8A49251AB129157F9894A036487" + + "090807060504030201").HexToByteArray(); + + writer.WriteBitString(containsSignature.AsReadOnlySpan().Slice(9, 256)); + + // certificate + writer.PopSequence(); + + Assert.Equal( + ComprehensiveReadTests.MicrosoftDotComSslCertBytes.ByteArrayToHex(), + writer.Encode().ByteArrayToHex()); + } + + private static void WriteRdn(AsnWriter writer, string oid, string value, UniversalTagNumber valueType) + { + writer.PushSetOf(); + writer.PushSequence(); + writer.WriteObjectIdentifier(oid); + writer.WriteCharacterString(valueType, value); + writer.PopSequence(); + writer.PopSetOf(); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/PushPopSequence.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/PushPopSequence.cs new file mode 100644 index 0000000000..50ef39b5d0 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/PushPopSequence.cs @@ -0,0 +1,514 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class PushPopSequence : Asn1WriterTests + { + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PopNewWriter(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + // Maybe ArgumentException isn't right for this, since no argument was provided. + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PopSequence()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PopNewWriter_CustomTag(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PopSequence(new Asn1Tag(TagClass.ContextSpecific, (int)ruleSet, true))); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PopBalancedWriter(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSequence(); + writer.PopSequence(); + + // Maybe ArgumentException isn't right for this, since no argument was provided. + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PopSequence()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PopBalancedWriter_CustomTag(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSequence(); + writer.PopSequence(); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PopSequence(new Asn1Tag(TagClass.ContextSpecific, (int)ruleSet, true))); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushCustom_PopStandard(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)ruleSet, true)); + + // Maybe ArgumentException isn't right for this, since no argument was provided. + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PopSequence()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushStandard_PopCustom(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSequence(); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PopSequence(new Asn1Tag(TagClass.ContextSpecific, (int)ruleSet, true))); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushPrimitive_PopStandard(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSequence(new Asn1Tag(UniversalTagNumber.Sequence)); + writer.PopSequence(); + + if (ruleSet == PublicEncodingRules.CER) + { + Verify(writer, "30800000"); + } + else + { + Verify(writer, "3000"); + } + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushCustomPrimitive_PopConstructed(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSequence(new Asn1Tag(TagClass.Private, 5)); + writer.PopSequence(new Asn1Tag(TagClass.Private, 5, true)); + + if (ruleSet == PublicEncodingRules.CER) + { + Verify(writer, "E5800000"); + } + else + { + Verify(writer, "E500"); + } + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushStandard_PopPrimitive(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSequence(); + writer.PopSequence(new Asn1Tag(UniversalTagNumber.Sequence, isConstructed: false)); + + if (ruleSet == PublicEncodingRules.CER) + { + Verify(writer, "30800000"); + } + else + { + Verify(writer, "3000"); + } + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushCustomConstructed_PopPrimitive(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSequence(new Asn1Tag(TagClass.Private, (int)ruleSet, true)); + writer.PopSequence(new Asn1Tag(TagClass.Private, (int)ruleSet)); + + byte tag = (byte)((int)ruleSet | 0b1110_0000); + string tagHex = tag.ToString("X2"); + string rest = ruleSet == PublicEncodingRules.CER ? "800000" : "00"; + + Verify(writer, tagHex + rest); + } + + [Fact] + public static void BER_WritesDefinite_Empty() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + writer.PushSequence(); + writer.PopSequence(); + + Verify(writer, "3000"); + } + + [Fact] + public static void CER_WritesIndefinite_Empty() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + writer.PushSequence(); + writer.PopSequence(); + + Verify(writer, "30800000"); + } + + [Fact] + public static void DER_WritesDefinite_CustomTag_Empty() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + writer.PushSequence(); + writer.PopSequence(); + + Verify(writer, "3000"); + } + + [Fact] + public static void BER_WritesDefinite_CustomTag__Empty() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag tag = new Asn1Tag(TagClass.Private, 15, true); + writer.PushSequence(tag); + writer.PopSequence(tag); + + Verify(writer, "EF00"); + } + + [Fact] + public static void CER_WritesIndefinite_CustomTag__Empty() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + Asn1Tag tag = new Asn1Tag(TagClass.Application, 91, true); + writer.PushSequence(tag); + writer.PopSequence(tag); + + Verify(writer, "7F5B800000"); + } + + [Fact] + public static void DER_WritesDefinite_CustomTag__Empty() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 30, true); + writer.PushSequence(tag); + writer.PopSequence(tag); + + Verify(writer, "BE00"); + } + + private static void TestNested(AsnWriter writer, Asn1Tag alt, string expectedHex) + { + writer.PushSequence(); + { + writer.PushSequence(alt); + writer.PopSequence(alt); + + writer.PushSequence(); + { + writer.PushSequence(alt); + { + writer.PushSequence(); + writer.PopSequence(); + } + + writer.PopSequence(alt); + } + + writer.PopSequence(); + } + + writer.PopSequence(); + + Verify(writer, expectedHex); + } + + [Fact] + public static void BER_Nested() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag alt = new Asn1Tag(TagClass.Private, 127, true); + + TestNested(writer, alt, "300AFF7F003005FF7F023000"); + } + + [Fact] + public static void CER_Nested() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + Asn1Tag alt = new Asn1Tag(TagClass.ContextSpecific, 12, true); + + TestNested(writer, alt, "3080AC8000003080AC8030800000000000000000"); + } + + [Fact] + public static void DER_Nested() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + Asn1Tag alt = new Asn1Tag(TagClass.Application, 5, true); + + TestNested(writer, alt, "30086500300465023000"); + } + + private static void SimpleContentShift(AsnWriter writer, string expectedHex) + { + writer.PushSequence(); + + // F00DF00D...F00DF00D + byte[] contentBytes = new byte[126]; + + for (int i = 0; i < contentBytes.Length; i += 2) + { + contentBytes[i] = 0xF0; + contentBytes[i + 1] = 0x0D; + } + + writer.WriteOctetString(contentBytes); + writer.PopSequence(); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.DER)] + public static void SimpleContentShift(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + const string ExpectedHex = + "308180" + + "047E" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D"; + + SimpleContentShift(writer, ExpectedHex); + } + + [Fact] + public static void SimpleContentShift_CER() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + + const string ExpectedHex = + "3080" + + "047E" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "0000"; + + SimpleContentShift(writer, ExpectedHex); + } + + private static void WriteRSAPublicKey(AsnEncodingRules ruleSet, string expectedHex) + { + AsnWriter innerWriter = new AsnWriter(ruleSet); + + byte[] paddedBigEndianN = ( + "00" + + "AF81C1CBD8203F624A539ED6608175372393A2837D4890E48A19DED369731156" + + "20968D6BE0D3DAA38AA777BE02EE0B6B93B724E8DCC12B632B4FA80BBC925BCE" + + "624F4CA7CC606306B39403E28C932D24DD546FFE4EF6A37F10770B2215EA8CBB" + + "5BF427E8C4D89B79EB338375100C5F83E55DE9B4466DDFBEEE42539AEF33EF18" + + "7B7760C3B1A1B2103C2D8144564A0C1039A09C85CF6B5974EB516FC8D6623C94" + + "AE3A5A0BB3B4C792957D432391566CF3E2A52AFB0C142B9E0681B8972671AF2B" + + "82DD390A39B939CF719568687E4990A63050CA7768DCD6B378842F18FDB1F6D9" + + "FF096BAF7BEB98DCF930D66FCFD503F58D41BFF46212E24E3AFC45EA42BD8847").HexToByteArray(); + + // Now it's padded little-endian. + Array.Reverse(paddedBigEndianN); + BigInteger n = new BigInteger(paddedBigEndianN); + const long e = 8589935681; + + innerWriter.PushSequence(); + innerWriter.WriteInteger(n); + innerWriter.WriteInteger(e); + innerWriter.PopSequence(); + + AsnWriter outerWriter = new AsnWriter(ruleSet); + // RSAPublicKey + outerWriter.PushSequence(); + + // AlgorithmIdentifier + outerWriter.PushSequence(); + outerWriter.WriteObjectIdentifier("1.2.840.113549.1.1.1"); + outerWriter.WriteNull(); + outerWriter.PopSequence(); + + outerWriter.WriteBitString(innerWriter.Encode()); + outerWriter.PopSequence(); + + Verify(outerWriter, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.DER)] + public static void WriteRSAPublicKey(PublicEncodingRules ruleSet) + { + const string ExpectedHex = + // CONSTRUCTED SEQUENCE + "30820124" + + // CONSTRUCTED SEQUENCE + "300D" + + // OBJECT IDENTIFIER (1.2.840.113549.1.1.1, rsaEncryption) + "06092A864886F70D010101" + + // NULL + "0500" + + // BIT STRING + "03820111" + + // 0 unused bits + "00" + + // sneaky inspection of the payload bytes + // CONSTRUCTED SEQUENCE + "3082010C" + + // INTEGER (n) + "02820101" + + "00AF81C1CBD8203F624A539ED6608175372393A2837D4890E48A19DED3697311" + + "5620968D6BE0D3DAA38AA777BE02EE0B6B93B724E8DCC12B632B4FA80BBC925B" + + "CE624F4CA7CC606306B39403E28C932D24DD546FFE4EF6A37F10770B2215EA8C" + + "BB5BF427E8C4D89B79EB338375100C5F83E55DE9B4466DDFBEEE42539AEF33EF" + + "187B7760C3B1A1B2103C2D8144564A0C1039A09C85CF6B5974EB516FC8D6623C" + + "94AE3A5A0BB3B4C792957D432391566CF3E2A52AFB0C142B9E0681B8972671AF" + + "2B82DD390A39B939CF719568687E4990A63050CA7768DCD6B378842F18FDB1F6" + + "D9FF096BAF7BEB98DCF930D66FCFD503F58D41BFF46212E24E3AFC45EA42BD88" + + "47" + + // INTEGER (e) + "02050200000441"; + + WriteRSAPublicKey((AsnEncodingRules)ruleSet, ExpectedHex); + } + + [Fact] + public static void WriteRSAPublicKey_CER() + { + const string ExpectedHex = + // CONSTRUCTED SEQUENCE + "3080" + + // CONSTRUCTED SEQUENCE + "3080" + + // OBJECT IDENTIFIER (1.2.840.113549.1.1.1, rsaEncryption) + "06092A864886F70D010101" + + // NULL + "0500" + + // End-of-Contents + "0000" + + // BIT STRING + "03820111" + + // 0 unused bits + "00" + + // sneaky inspection of the payload bytes + // CONSTRUCTED SEQUENCE + "3080" + + // INTEGER (n) + "02820101" + + "00AF81C1CBD8203F624A539ED6608175372393A2837D4890E48A19DED3697311" + + "5620968D6BE0D3DAA38AA777BE02EE0B6B93B724E8DCC12B632B4FA80BBC925B" + + "CE624F4CA7CC606306B39403E28C932D24DD546FFE4EF6A37F10770B2215EA8C" + + "BB5BF427E8C4D89B79EB338375100C5F83E55DE9B4466DDFBEEE42539AEF33EF" + + "187B7760C3B1A1B2103C2D8144564A0C1039A09C85CF6B5974EB516FC8D6623C" + + "94AE3A5A0BB3B4C792957D432391566CF3E2A52AFB0C142B9E0681B8972671AF" + + "2B82DD390A39B939CF719568687E4990A63050CA7768DCD6B378842F18FDB1F6" + + "D9FF096BAF7BEB98DCF930D66FCFD503F58D41BFF46212E24E3AFC45EA42BD88" + + "47" + + // INTEGER (e) + "02050200000441" + + // End-of-Contents + "0000" + + // (no EoC for the BIT STRING) + // End-of-Contents + "0000"; + + WriteRSAPublicKey(AsnEncodingRules.CER, ExpectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, false)] + [InlineData(PublicEncodingRules.CER, false)] + [InlineData(PublicEncodingRules.DER, false)] + [InlineData(PublicEncodingRules.BER, true)] + [InlineData(PublicEncodingRules.CER, true)] + [InlineData(PublicEncodingRules.DER, true)] + public static void CannotEncodeWhileUnbalanced(PublicEncodingRules ruleSet, bool customTag) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + if (customTag) + { + writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)ruleSet, true)); + } + else + { + writer.PushSequence(); + } + + int written = -5; + + Assert.Throws<InvalidOperationException>(() => writer.Encode()); + Assert.Throws<InvalidOperationException>(() => writer.TryEncode(Span<byte>.Empty, out written)); + Assert.Equal(-5, written); + + byte[] buf = new byte[10]; + Assert.Throws<InvalidOperationException>(() => writer.TryEncode(buf, out written)); + Assert.Equal(-5, written); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushSequence_EndOfContents(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PushSequence(Asn1Tag.EndOfContents)); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/PushPopSetOf.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/PushPopSetOf.cs new file mode 100644 index 0000000000..9ed7155c6a --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/PushPopSetOf.cs @@ -0,0 +1,497 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class PushPopSetOf : Asn1WriterTests + { + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PopNewWriter(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + // Maybe ArgumentException isn't right for this, since no argument was provided. + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PopSetOf()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PopNewWriter_CustomTag(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PopSetOf(new Asn1Tag(TagClass.ContextSpecific, (int)ruleSet, true))); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PopBalancedWriter(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSetOf(); + writer.PopSetOf(); + + // Maybe ArgumentException isn't right for this, since no argument was provided. + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PopSetOf()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PopBalancedWriter_CustomTag(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSetOf(); + writer.PopSetOf(); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PopSetOf(new Asn1Tag(TagClass.ContextSpecific, (int)ruleSet, true))); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushCustom_PopStandard(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSetOf(new Asn1Tag(TagClass.ContextSpecific, (int)ruleSet, true)); + + // Maybe ArgumentException isn't right for this, since no argument was provided. + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PopSetOf()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushStandard_PopCustom(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSetOf(); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PopSetOf(new Asn1Tag(TagClass.ContextSpecific, (int)ruleSet, true))); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushPrimitive_PopStandard(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSetOf(new Asn1Tag(UniversalTagNumber.SetOf)); + writer.PopSetOf(); + + if (ruleSet == PublicEncodingRules.CER) + { + Verify(writer, "31800000"); + } + else + { + Verify(writer, "3100"); + } + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushCustomPrimitive_PopConstructed(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSetOf(new Asn1Tag(TagClass.Private, 5)); + writer.PopSetOf(new Asn1Tag(TagClass.Private, 5, true)); + + if (ruleSet == PublicEncodingRules.CER) + { + Verify(writer, "E5800000"); + } + else + { + Verify(writer, "E500"); + } + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushStandard_PopPrimitive(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSetOf(); + writer.PopSetOf(new Asn1Tag(UniversalTagNumber.SetOf)); + + if (ruleSet == PublicEncodingRules.CER) + { + Verify(writer, "31800000"); + } + else + { + Verify(writer, "3100"); + } + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushCustomConstructed_PopPrimitive(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.PushSetOf(new Asn1Tag(TagClass.Private, (int)ruleSet, true)); + writer.PopSetOf(new Asn1Tag(TagClass.Private, (int)ruleSet)); + + byte tag = (byte)((int)ruleSet | 0b1110_0000); + string tagHex = tag.ToString("X2"); + string rest = ruleSet == PublicEncodingRules.CER ? "800000" : "00"; + + Verify(writer, tagHex + rest); + } + + [Fact] + public static void BER_WritesDefinite_Empty() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + writer.PushSetOf(); + writer.PopSetOf(); + + Verify(writer, "3100"); + } + + [Fact] + public static void CER_WritesIndefinite_Empty() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + writer.PushSetOf(); + writer.PopSetOf(); + + Verify(writer, "31800000"); + } + + [Fact] + public static void DER_WritesDefinite_CustomTag_Empty() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + writer.PushSetOf(); + writer.PopSetOf(); + + Verify(writer, "3100"); + } + + [Fact] + public static void BER_WritesDefinite_CustomTag__Empty() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag tag = new Asn1Tag(TagClass.Private, 15, true); + writer.PushSetOf(tag); + writer.PopSetOf(tag); + + Verify(writer, "EF00"); + } + + [Fact] + public static void CER_WritesIndefinite_CustomTag__Empty() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + Asn1Tag tag = new Asn1Tag(TagClass.Application, 91, true); + writer.PushSetOf(tag); + writer.PopSetOf(tag); + + Verify(writer, "7F5B800000"); + } + + [Fact] + public static void DER_WritesDefinite_CustomTag__Empty() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 30, true); + writer.PushSetOf(tag); + writer.PopSetOf(tag); + + Verify(writer, "BE00"); + } + + private static void TestNested(AsnWriter writer, Asn1Tag alt, string expectedHex) + { + // Written in pre-sorted order, since sorting is a different test. + writer.PushSetOf(); + { + writer.PushSetOf(); + { + writer.PushSetOf(alt); + { + writer.PushSetOf(); + writer.PopSetOf(); + } + + writer.PopSetOf(alt); + } + + writer.PopSetOf(); + + writer.PushSetOf(alt); + writer.PopSetOf(alt); + } + + writer.PopSetOf(); + + Verify(writer, expectedHex); + } + + [Fact] + public static void BER_Nested() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag alt = new Asn1Tag(TagClass.Private, 127, true); + + TestNested(writer, alt, "310A3105FF7F023100FF7F00"); + } + + [Fact] + public static void CER_Nested() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + Asn1Tag alt = new Asn1Tag(TagClass.ContextSpecific, 12, true); + + TestNested(writer, alt, "31803180AC803180000000000000AC8000000000"); + } + + [Fact] + public static void DER_Nested() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + Asn1Tag alt = new Asn1Tag(TagClass.Application, 5, true); + + TestNested(writer, alt, "31083104650231006500"); + } + + private static void SimpleContentShift(AsnWriter writer, string expectedHex) + { + writer.PushSetOf(); + + // F00DF00D...F00DF00D + byte[] contentBytes = new byte[126]; + + for (int i = 0; i < contentBytes.Length; i += 2) + { + contentBytes[i] = 0xF0; + contentBytes[i + 1] = 0x0D; + } + + writer.WriteOctetString(contentBytes); + writer.PopSetOf(); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.DER)] + public static void SimpleContentShift(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + const string ExpectedHex = + "318180" + + "047E" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D"; + + SimpleContentShift(writer, ExpectedHex); + } + + [Fact] + public static void SimpleContentShift_CER() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + + const string ExpectedHex = + "3180" + + "047E" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "F00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00DF00D" + + "0000"; + + SimpleContentShift(writer, ExpectedHex); + } + + private static void ValidateDataSorting(AsnWriter writer, string expectedHex) + { + writer.PushSetOf(); + + // 02 01 FF + writer.WriteInteger(-1); + // 02 01 00 + writer.WriteInteger(0); + // 02 02 00 FF + writer.WriteInteger(255); + // 01 01 FF + writer.WriteBoolean(true); + // 45 01 00 + writer.WriteBoolean(new Asn1Tag(TagClass.Application, 5), false); + // 02 01 7F + writer.WriteInteger(127); + // 02 01 80 + writer.WriteInteger(sbyte.MinValue); + // 02 02 00 FE + writer.WriteInteger(254); + // 02 01 00 + writer.WriteInteger(0); + + writer.PopSetOf(); + + // The correct sort order (CER, DER) is + // Universal Boolean: true + // Universal Integer: 0 + // Universal Integer: 0 + // Universal Integer: 127 + // Universal Integer: -128 + // Universal Integer: -1 + // Universal Integer: 254 + // Universal Integer: 255 + // Application 5 (Boolean): false + + // This test would be + // + // GrabBag ::= SET OF GrabBagItem + // + // GrabBagItem ::= CHOICE ( + // value INTEGER + // bool BOOLEAN + // grr [APPLICATION 5] IMPLICIT BOOLEAN + // ) + + Verify(writer, expectedHex); + } + + [Fact] + public static void BER_DoesNotSort() + { + const string ExpectedHex = + "311D" + + "0201FF" + + "020100" + + "020200FF" + + "0101FF" + + "450100" + + "02017F" + + "020180" + + "020200FE" + + "020100"; + + ValidateDataSorting(new AsnWriter(AsnEncodingRules.BER), ExpectedHex); + } + + [Fact] + public static void CER_SortsData() + { + const string ExpectedHex = + "3180" + + "0101FF" + + "020100" + + "020100" + + "02017F" + + "020180" + + "0201FF" + + "020200FE" + + "020200FF" + + "450100" + + "0000"; + + ValidateDataSorting(new AsnWriter(AsnEncodingRules.CER), ExpectedHex); + } + + [Fact] + public static void DER_SortsData() + { + const string ExpectedHex = + "311D" + + "0101FF" + + "020100" + + "020100" + + "02017F" + + "020180" + + "0201FF" + + "020200FE" + + "020200FF" + + "450100"; + + ValidateDataSorting(new AsnWriter(AsnEncodingRules.DER), ExpectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, false)] + [InlineData(PublicEncodingRules.CER, false)] + [InlineData(PublicEncodingRules.DER, false)] + [InlineData(PublicEncodingRules.BER, true)] + [InlineData(PublicEncodingRules.CER, true)] + [InlineData(PublicEncodingRules.DER, true)] + public static void CannotEncodeWhileUnbalanced(PublicEncodingRules ruleSet, bool customTag) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + if (customTag) + { + writer.PushSetOf(new Asn1Tag(TagClass.ContextSpecific, (int)ruleSet, true)); + } + else + { + writer.PushSetOf(); + } + + int written = -5; + + Assert.Throws<InvalidOperationException>(() => writer.Encode()); + Assert.Throws<InvalidOperationException>(() => writer.TryEncode(Span<byte>.Empty, out written)); + Assert.Equal(-5, written); + + byte[] buf = new byte[10]; + Assert.Throws<InvalidOperationException>(() => writer.TryEncode(buf, out written)); + Assert.Equal(-5, written); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void PushSetOf_EndOfContents(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.PushSetOf(Asn1Tag.EndOfContents)); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteBMPString.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteBMPString.cs new file mode 100644 index 0000000000..12c44833e2 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteBMPString.cs @@ -0,0 +1,294 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Security.Cryptography.Asn1; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteBMPString : WriteCharacterString + { + public static IEnumerable<object[]> ShortValidCases { get; } = new object[][] + { + new object[] + { + string.Empty, + "00", + }, + new object[] + { + "hi", + "0400680069", + }, + new object[] + { + "Dr. & Mrs. Smith\u2010Jones \uFE60 children", + "42" + + "00440072002E002000260020004D00720073002E00200053006D0069" + + "007400682010004A006F006E006500730020FE600020006300680069" + + "006C006400720065006E", + }, + }; + + public static IEnumerable<object[]> LongValidCases { get; } = new object[][] + { + new object[] + { + // 498 Han-fragrant, 497 Han-dark, 23 Han-rich + new string('\u9999', 498) + new string('\u6666', 497) + new string('\u4444', 23), + "8207F4" + new string('9', 498 * 4) + new string('6', 497 * 4) + new string('4', 23 * 4), + }, + }; + + public static IEnumerable<object[]> CERSegmentedCases { get; } = new object[][] + { + new object[] + { + GettysburgAddress, + 1458 * 2, + }, + new object[] + { + // 498 Han-fragrant, 497 Han-dark, 5 Han-rich + new string('\u9999', 498) + new string('\u6666', 497) + new string('\u4444', 5), + 2000, + }, + }; + + public static IEnumerable<object[]> InvalidInputs { get; } = new object[][] + { + // Surrogate pair for "Deseret Small Letter Yee" (U+10437) + new object[] { "\uD801\uDC37" }, + }; + + internal override void WriteString(AsnWriter writer, string s) => + writer.WriteCharacterString(UniversalTagNumber.BMPString, s); + + internal override void WriteString(AsnWriter writer, Asn1Tag tag, string s) => + writer.WriteCharacterString(tag, UniversalTagNumber.BMPString, s); + + internal override void WriteSpan(AsnWriter writer, ReadOnlySpan<char> s) => + writer.WriteCharacterString(UniversalTagNumber.BMPString, s); + + internal override void WriteSpan(AsnWriter writer, Asn1Tag tag, ReadOnlySpan<char> s) => + writer.WriteCharacterString(tag, UniversalTagNumber.BMPString, s); + + internal override Asn1Tag StandardTag => new Asn1Tag(UniversalTagNumber.BMPString); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_String(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_String(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_String_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_String_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_String(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_String(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_String_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_String_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_String(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_String(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_String_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_String_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_Span(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_Span(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_Span_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_Span_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_Span(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_Span(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_Span_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_Span_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_Span(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_Span(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_Span_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_Span_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_String_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_String_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_String_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_String_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_Span_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_Span_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_Span_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_Span_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_String_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_String_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_String_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_String_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_Span_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_Span_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_Span_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_Span_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_String_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_String_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_String_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_String_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_Span_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_Span_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_Span_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_Span_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public new void VerifyWrite_String_Null(PublicEncodingRules ruleSet) => + base.VerifyWrite_String_Null(ruleSet); + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public new void VerifyWrite_String_Null_CustomTag(PublicEncodingRules ruleSet) => + base.VerifyWrite_String_Null_CustomTag(ruleSet); + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public new void VerifyWrite_EndOfContents_String(PublicEncodingRules ruleSet) => + base.VerifyWrite_EndOfContents_String(ruleSet); + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public new void VerifyWrite_EndOfContents_Span(PublicEncodingRules ruleSet) => + base.VerifyWrite_EndOfContents_Span(ruleSet); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_String(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_String(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_String_CustomTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_String_CustomTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_String_ConstructedTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_String_ConstructedTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_String_CustomPrimitiveTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_String_CustomPrimitiveTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_Span(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_Span(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_Span_CustomTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_Span_CustomTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_Span_ConstructedTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_Span_ConstructedTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_Span_CustomPrimitiveTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_Span_CustomPrimitiveTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(InvalidInputs))] + public new void VerifyWrite_String_NonEncodable(string input) => + base.VerifyWrite_String_NonEncodable(input); + + [Theory] + [MemberData(nameof(InvalidInputs))] + public new void VerifyWrite_Span_NonEncodable(string input) => + base.VerifyWrite_Span_NonEncodable(input); + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteBitString.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteBitString.cs new file mode 100644 index 0000000000..2a0c4dd008 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteBitString.cs @@ -0,0 +1,305 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteBitString : Asn1WriterTests + { + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void WriteEmpty(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteBitString(ReadOnlySpan<byte>.Empty); + + Verify(writer, "030100"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, 1, 1, "030201")] + [InlineData(PublicEncodingRules.CER, 2, 1, "030301")] + [InlineData(PublicEncodingRules.DER, 3, 1, "030401")] + [InlineData(PublicEncodingRules.BER, 126, 0, "037F00")] + [InlineData(PublicEncodingRules.CER, 127, 3, "03818003")] + [InlineData(PublicEncodingRules.BER, 999, 0, "038203E800")] + [InlineData(PublicEncodingRules.CER, 999, 0, "038203E800")] + [InlineData(PublicEncodingRules.DER, 999, 0, "038203E800")] + [InlineData(PublicEncodingRules.BER, 1000, 0, "038203E900")] + [InlineData(PublicEncodingRules.DER, 1000, 0, "038203E900")] + [InlineData(PublicEncodingRules.BER, 2000, 0, "038207D100")] + [InlineData(PublicEncodingRules.DER, 2000, 0, "038207D100")] + public void WritePrimitive(PublicEncodingRules ruleSet, int length, int unusedBitCount, string hexStart) + { + string payloadHex = new string('0', 2 * length); + string expectedHex = hexStart + payloadHex; + byte[] data = new byte[length]; + + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteBitString(data, unusedBitCount); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(1000, 1, "2380038203E800", "030201")] + [InlineData(999*2, 3, "2380038203E800", "038203E803")] + public void WriteSegmentedCER(int length, int unusedBitCount, string hexStart, string hexStart2) + { + string payload1Hex = new string('8', 999 * 2); + string payload2Hex = new string('8', (length - 999) * 2); + string expectedHex = hexStart + payload1Hex + hexStart2 + payload2Hex + "0000"; + byte[] data = new byte[length]; + + for (int i = 0; i < data.Length; i++) + { + data[i] = 0x88; + } + + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + writer.WriteBitString(data, unusedBitCount); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, 0, false)] + [InlineData(PublicEncodingRules.CER, 0, false)] + [InlineData(PublicEncodingRules.DER, 0, false)] + [InlineData(PublicEncodingRules.BER, 999, false)] + [InlineData(PublicEncodingRules.CER, 999, false)] + [InlineData(PublicEncodingRules.DER, 999, false)] + [InlineData(PublicEncodingRules.BER, 1000, false)] + [InlineData(PublicEncodingRules.CER, 1000, true)] + [InlineData(PublicEncodingRules.DER, 1000, false)] + [InlineData(PublicEncodingRules.BER, 1998, false)] + [InlineData(PublicEncodingRules.CER, 1998, true)] + [InlineData(PublicEncodingRules.BER, 4096, false)] + [InlineData(PublicEncodingRules.CER, 4096, true)] + [InlineData(PublicEncodingRules.DER, 4096, false)] + public void VerifyWriteBitString_PrimitiveOrConstructed( + PublicEncodingRules ruleSet, + int payloadLength, + bool expectConstructed) + { + byte[] data = new byte[payloadLength]; + + Asn1Tag[] tagsToTry = + { + new Asn1Tag(UniversalTagNumber.BitString), + new Asn1Tag(UniversalTagNumber.BitString, isConstructed: true), + new Asn1Tag(TagClass.Private, 87), + new Asn1Tag(TagClass.ContextSpecific, 13, isConstructed: true), + }; + + byte[] answerBuf = new byte[payloadLength + 100]; + + foreach (Asn1Tag toTry in tagsToTry) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteBitString(toTry, data); + + Assert.True(writer.TryEncode(answerBuf, out _)); + Assert.True(Asn1Tag.TryParse(answerBuf, out Asn1Tag writtenTag, out _)); + + if (expectConstructed) + { + Assert.True(writtenTag.IsConstructed, $"writtenTag.IsConstructed ({toTry})"); + } + else + { + Assert.False(writtenTag.IsConstructed, $"writtenTag.IsConstructed ({toTry})"); + } + + Assert.Equal(toTry.TagClass, writtenTag.TagClass); + Assert.Equal(toTry.TagValue, writtenTag.TagValue); + } + } + + [Theory] + [InlineData(PublicEncodingRules.BER, 0, "FF", false)] + [InlineData(PublicEncodingRules.BER, 1, "FE", false)] + [InlineData(PublicEncodingRules.CER, 1, "FE", false)] + [InlineData(PublicEncodingRules.DER, 1, "FE", false)] + [InlineData(PublicEncodingRules.BER, 1, "FF", true)] + [InlineData(PublicEncodingRules.CER, 1, "FF", true)] + [InlineData(PublicEncodingRules.DER, 1, "FF", true)] + [InlineData(PublicEncodingRules.BER, 7, "C0", true)] + [InlineData(PublicEncodingRules.CER, 7, "C0", true)] + [InlineData(PublicEncodingRules.DER, 7, "C0", true)] + [InlineData(PublicEncodingRules.BER, 7, "80", false)] + [InlineData(PublicEncodingRules.CER, 7, "80", false)] + [InlineData(PublicEncodingRules.DER, 7, "80", false)] + [InlineData(PublicEncodingRules.DER, 7, "40", true)] + [InlineData(PublicEncodingRules.DER, 6, "40", false)] + [InlineData(PublicEncodingRules.DER, 6, "C0", false)] + [InlineData(PublicEncodingRules.DER, 6, "20", true)] + [InlineData(PublicEncodingRules.DER, 5, "20", false)] + [InlineData(PublicEncodingRules.DER, 5, "A0", false)] + [InlineData(PublicEncodingRules.DER, 5, "10", true)] + [InlineData(PublicEncodingRules.DER, 4, "10", false)] + [InlineData(PublicEncodingRules.DER, 4, "90", false)] + [InlineData(PublicEncodingRules.DER, 4, "30", false)] + [InlineData(PublicEncodingRules.DER, 4, "08", true)] + [InlineData(PublicEncodingRules.DER, 4, "88", true)] + [InlineData(PublicEncodingRules.DER, 3, "08", false)] + [InlineData(PublicEncodingRules.DER, 3, "A8", false)] + [InlineData(PublicEncodingRules.DER, 3, "04", true)] + [InlineData(PublicEncodingRules.DER, 3, "14", true)] + [InlineData(PublicEncodingRules.DER, 2, "04", false)] + [InlineData(PublicEncodingRules.DER, 2, "0C", false)] + [InlineData(PublicEncodingRules.DER, 2, "FC", false)] + [InlineData(PublicEncodingRules.DER, 2, "02", true)] + [InlineData(PublicEncodingRules.DER, 2, "82", true)] + [InlineData(PublicEncodingRules.DER, 2, "FE", true)] + [InlineData(PublicEncodingRules.DER, 1, "02", false)] + [InlineData(PublicEncodingRules.DER, 1, "82", false)] + [InlineData(PublicEncodingRules.DER, 1, "FE", false)] + [InlineData(PublicEncodingRules.DER, 1, "80", false)] + public static void WriteBitString_UnusedBitCount_MustBeValid( + PublicEncodingRules ruleSet, + int unusedBitCount, + string inputHex, + bool expectThrow) + { + byte[] inputBytes = inputHex.HexToByteArray(); + + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + if (expectThrow) + { + Assert.Throws<CryptographicException>( + () => writer.WriteBitString(inputBytes, unusedBitCount)); + + Assert.Throws<CryptographicException>( + () => writer.WriteBitString(new Asn1Tag(TagClass.ContextSpecific, 3), inputBytes, unusedBitCount)); + + return; + } + + byte[] output = new byte[512]; + writer.WriteBitString(inputBytes, unusedBitCount); + Assert.True(writer.TryEncode(output, out int bytesWritten)); + + // This assumes that inputBytes is never more than 999 (and avoids CER constructed forms) + Assert.Equal(unusedBitCount, output[bytesWritten - inputBytes.Length - 1]); + + writer.WriteBitString(new Asn1Tag(TagClass.ContextSpecific, 9), inputBytes, unusedBitCount); + Assert.True(writer.TryEncode(output, out bytesWritten)); + + Assert.Equal(unusedBitCount, output[bytesWritten - inputBytes.Length - 1]); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, -1)] + [InlineData(PublicEncodingRules.CER, -1)] + [InlineData(PublicEncodingRules.DER, -1)] + [InlineData(PublicEncodingRules.BER, -2)] + [InlineData(PublicEncodingRules.CER, -2)] + [InlineData(PublicEncodingRules.DER, -2)] + [InlineData(PublicEncodingRules.BER, 8)] + [InlineData(PublicEncodingRules.CER, 8)] + [InlineData(PublicEncodingRules.DER, 8)] + [InlineData(PublicEncodingRules.BER, 9)] + [InlineData(PublicEncodingRules.CER, 9)] + [InlineData(PublicEncodingRules.DER, 9)] + [InlineData(PublicEncodingRules.BER, 1048576)] + [InlineData(PublicEncodingRules.CER, 1048576)] + [InlineData(PublicEncodingRules.DER, 1048576)] + public static void UnusedBitCounts_Bounds(PublicEncodingRules ruleSet, int unusedBitCount) + { + byte[] data = new byte[5]; + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + ArgumentOutOfRangeException exception = AssertExtensions.Throws<ArgumentOutOfRangeException>( + nameof(unusedBitCount), + () => writer.WriteBitString(data, unusedBitCount)); + + Assert.Equal(unusedBitCount, exception.ActualValue); + + exception = AssertExtensions.Throws<ArgumentOutOfRangeException>( + nameof(unusedBitCount), + () => writer.WriteBitString(new Asn1Tag(TagClass.ContextSpecific, 5), data, unusedBitCount)); + + Assert.Equal(unusedBitCount, exception.ActualValue); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void EmptyData_Requires0UnusedBits(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => writer.WriteBitString(ReadOnlySpan<byte>.Empty, 1)); + + Assert.Throws<CryptographicException>( + () => writer.WriteBitString(ReadOnlySpan<byte>.Empty, 7)); + + Asn1Tag contextTag = new Asn1Tag(TagClass.ContextSpecific, 19); + + Assert.Throws<CryptographicException>( + () => writer.WriteBitString(contextTag, ReadOnlySpan<byte>.Empty, 1)); + + Assert.Throws<CryptographicException>( + () => writer.WriteBitString(contextTag, ReadOnlySpan<byte>.Empty, 7)); + + writer.WriteBitString(ReadOnlySpan<byte>.Empty, 0); + writer.WriteBitString(contextTag, ReadOnlySpan<byte>.Empty, 0); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, PublicTagClass.Universal, 3, "030100")] + [InlineData(PublicEncodingRules.CER, PublicTagClass.Universal, 3, "030100")] + [InlineData(PublicEncodingRules.DER, PublicTagClass.Universal, 3, "030100")] + [InlineData(PublicEncodingRules.BER, PublicTagClass.Private, 1, "C10100")] + [InlineData(PublicEncodingRules.CER, PublicTagClass.Application, 5, "450100")] + [InlineData(PublicEncodingRules.DER, PublicTagClass.ContextSpecific, 32, "9F200100")] + public static void EmptyData_Allows0UnusedBits( + PublicEncodingRules ruleSet, + PublicTagClass tagClass, + int tagValue, + string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + if (tagClass == PublicTagClass.Universal) + { + Debug.Assert(tagValue == (int)UniversalTagNumber.BitString); + writer.WriteBitString(ReadOnlySpan<byte>.Empty, 0); + } + else + { + writer.WriteBitString(new Asn1Tag((TagClass)tagClass, tagValue), ReadOnlySpan<byte>.Empty, 0); + } + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyWriteBitString_EndOfContents(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteBitString(Asn1Tag.EndOfContents, ReadOnlySpan<byte>.Empty)); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteBitString(Asn1Tag.EndOfContents, new byte[1])); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteBoolean.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteBoolean.cs new file mode 100644 index 0000000000..d52251dd4f --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteBoolean.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteBoolean : Asn1WriterTests + { + [Theory] + [InlineData(PublicEncodingRules.BER, false, "010100")] + [InlineData(PublicEncodingRules.BER, true, "0101FF")] + [InlineData(PublicEncodingRules.CER, false, "010100")] + [InlineData(PublicEncodingRules.CER, true, "0101FF")] + [InlineData(PublicEncodingRules.DER, false, "010100")] + [InlineData(PublicEncodingRules.DER, true, "0101FF")] + public void VerifyWriteBoolean(PublicEncodingRules ruleSet, bool value, string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteBoolean(value); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, false, "830100")] + [InlineData(PublicEncodingRules.BER, true, "8301FF")] + [InlineData(PublicEncodingRules.CER, false, "830100")] + [InlineData(PublicEncodingRules.CER, true, "8301FF")] + [InlineData(PublicEncodingRules.DER, false, "830100")] + [InlineData(PublicEncodingRules.DER, true, "8301FF")] + public void VerifyWriteBoolean_Context3(PublicEncodingRules ruleSet, bool value, string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteBoolean(new Asn1Tag(TagClass.ContextSpecific, 3), value); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, false)] + [InlineData(PublicEncodingRules.BER, true)] + [InlineData(PublicEncodingRules.CER, false)] + [InlineData(PublicEncodingRules.CER, true)] + [InlineData(PublicEncodingRules.DER, false)] + [InlineData(PublicEncodingRules.DER, true)] + public void VerifyWriteBoolean_EndOfContents(PublicEncodingRules ruleSet, bool value) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteBoolean(Asn1Tag.EndOfContents, value)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, false)] + [InlineData(PublicEncodingRules.BER, true)] + [InlineData(PublicEncodingRules.CER, false)] + [InlineData(PublicEncodingRules.CER, true)] + [InlineData(PublicEncodingRules.DER, false)] + [InlineData(PublicEncodingRules.DER, true)] + public void VerifyWriteBoolean_ConstructedIgnored(PublicEncodingRules ruleSet, bool value) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteBoolean(new Asn1Tag(TagClass.ContextSpecific, 7, true), value); + writer.WriteBoolean(new Asn1Tag(UniversalTagNumber.Boolean, true), value); + + if (value) + { + Verify(writer, "8701FF0101FF"); + } + else + { + Verify(writer, "870100010100"); + } + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteCharacterString.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteCharacterString.cs new file mode 100644 index 0000000000..fd0c579368 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteCharacterString.cs @@ -0,0 +1,437 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using System.Text; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public abstract class WriteCharacterString : Asn1WriterTests + { + internal abstract void WriteString(AsnWriter writer, string s); + internal abstract void WriteString(AsnWriter writer, Asn1Tag tag, string s); + + internal abstract void WriteSpan(AsnWriter writer, ReadOnlySpan<char> s); + internal abstract void WriteSpan(AsnWriter writer, Asn1Tag tag, ReadOnlySpan<char> s); + + internal abstract Asn1Tag StandardTag { get; } + + protected const string GettysburgAddress = + "Four score and seven years ago our fathers brought forth on this continent, a new nation, " + + "conceived in Liberty, and dedicated to the proposition that all men are created equal.\r\n" + + "\r\n" + + "Now we are engaged in a great civil war, testing whether that nation, or any nation so " + + "conceived and so dedicated, can long endure. We are met on a great battle-field of that " + + "war. We have come to dedicate a portion of that field, as a final resting place for those " + + "who here gave their lives that that nation might live. It is altogether fitting and proper " + + "that we should do this.\r\n" + + "\r\n" + + "But, in a larger sense, we can not dedicate-we can not consecrate-we can not hallow-this " + + "ground. The brave men, living and dead, who struggled here, have consecrated it, far above " + + "our poor power to add or detract. The world will little note, nor long remember what we say " + + "here, but it can never forget what they did here. It is for us the living, rather, to be " + + "dedicated here to the unfinished work which they who fought here have thus far so nobly " + + "advanced. It is rather for us to be here dedicated to the great task remaining before " + + "us-that from these honored dead we take increased devotion to that cause for which they " + + "gave the last full measure of devotion-that we here highly resolve that these dead shall " + + "not have died in vain-that this nation, under God, shall have a new birth of freedom-and " + + "that government of the people, by the people, for the people, shall not perish from the earth."; + + protected void VerifyWrite_BER_String(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + WriteString(writer, input); + + Verify(writer, Stringify(StandardTag) + expectedPayloadHex); + } + + protected void VerifyWrite_BER_String_CustomTag(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 14); + WriteString(writer, tag, input); + + Verify(writer, Stringify(tag) + expectedPayloadHex); + } + + protected void VerifyWrite_CER_String(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + WriteString(writer, input); + + Verify(writer, Stringify(StandardTag) + expectedPayloadHex); + } + + protected void VerifyWrite_CER_String_CustomTag(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag tag = new Asn1Tag(TagClass.Private, 19); + WriteString(writer, tag, input); + + Verify(writer, Stringify(tag) + expectedPayloadHex); + } + + protected void VerifyWrite_DER_String(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + WriteString(writer, input); + + Verify(writer, Stringify(StandardTag) + expectedPayloadHex); + } + + protected void VerifyWrite_DER_String_CustomTag(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag tag = new Asn1Tag(TagClass.Application, 2); + WriteString(writer, tag, input); + + Verify(writer, Stringify(tag) + expectedPayloadHex); + } + + protected void VerifyWrite_BER_Span(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + WriteSpan(writer, input.AsReadOnlySpan()); + + Verify(writer, Stringify(StandardTag) + expectedPayloadHex); + } + + protected void VerifyWrite_BER_Span_CustomTag(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag tag = new Asn1Tag(TagClass.Private, int.MaxValue >> 1); + WriteSpan(writer, tag, input.AsReadOnlySpan()); + + Verify(writer, Stringify(tag) + expectedPayloadHex); + } + + protected void VerifyWrite_CER_Span(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + WriteSpan(writer, input.AsReadOnlySpan()); + + Verify(writer, Stringify(StandardTag) + expectedPayloadHex); + } + + protected void VerifyWrite_CER_Span_CustomTag(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag tag = new Asn1Tag(TagClass.Application, 30); + WriteSpan(writer, tag, input.AsReadOnlySpan()); + + Verify(writer, Stringify(tag) + expectedPayloadHex); + } + + protected void VerifyWrite_DER_Span(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + WriteSpan(writer, input.AsReadOnlySpan()); + + Verify(writer, Stringify(StandardTag) + expectedPayloadHex); + } + + protected void VerifyWrite_DER_Span_CustomTag(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 31); + WriteSpan(writer, tag, input.AsReadOnlySpan()); + + Verify(writer, Stringify(tag) + expectedPayloadHex); + } + + protected void VerifyWrite_BER_String_ClearsConstructed(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag standard = StandardTag; + Asn1Tag tag = new Asn1Tag(standard.TagClass, standard.TagValue, isConstructed: true); + WriteString(writer, tag, input); + + Verify(writer, Stringify(standard) + expectedPayloadHex); + } + + protected void VerifyWrite_BER_String_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag tag = new Asn1Tag(TagClass.Application, 19, isConstructed: true); + Asn1Tag expected = new Asn1Tag(tag.TagClass, tag.TagValue); + WriteString(writer, tag, input); + + Verify(writer, Stringify(expected) + expectedPayloadHex); + } + + protected void VerifyWrite_BER_Span_ClearsConstructed(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag standard = StandardTag; + Asn1Tag tag = new Asn1Tag(standard.TagClass, standard.TagValue, isConstructed: true); + WriteSpan(writer, tag, input.AsReadOnlySpan()); + + Verify(writer, Stringify(standard) + expectedPayloadHex); + } + + protected void VerifyWrite_BER_Span_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag tag = new Asn1Tag(TagClass.Private, 24601, isConstructed: true); + Asn1Tag expected = new Asn1Tag(tag.TagClass, tag.TagValue); + WriteSpan(writer, tag, input.AsReadOnlySpan()); + + Verify(writer, Stringify(expected) + expectedPayloadHex); + } + + protected void VerifyWrite_CER_String_ClearsConstructed(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + Asn1Tag standard = StandardTag; + Asn1Tag tag = new Asn1Tag(standard.TagClass, standard.TagValue, isConstructed: true); + WriteString(writer, tag, input); + + Verify(writer, Stringify(standard) + expectedPayloadHex); + } + + protected void VerifyWrite_CER_String_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 1701, isConstructed: true); + Asn1Tag expected = new Asn1Tag(tag.TagClass, tag.TagValue); + WriteString(writer, tag, input); + + Verify(writer, Stringify(expected) + expectedPayloadHex); + } + + protected void VerifyWrite_CER_Span_ClearsConstructed(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + Asn1Tag standard = StandardTag; + Asn1Tag tag = new Asn1Tag(standard.TagClass, standard.TagValue, isConstructed: true); + WriteSpan(writer, tag, input.AsReadOnlySpan()); + + Verify(writer, Stringify(standard) + expectedPayloadHex); + } + + protected void VerifyWrite_CER_Span_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + Asn1Tag tag = new Asn1Tag(TagClass.Application, 11, isConstructed: true); + Asn1Tag expected = new Asn1Tag(tag.TagClass, tag.TagValue); + WriteSpan(writer, tag, input.AsReadOnlySpan()); + + Verify(writer, Stringify(expected) + expectedPayloadHex); + } + + protected void VerifyWrite_DER_String_ClearsConstructed(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + Asn1Tag standard = StandardTag; + Asn1Tag tag = new Asn1Tag(standard.TagClass, standard.TagValue, isConstructed: true); + WriteString(writer, tag, input); + + Verify(writer, Stringify(standard) + expectedPayloadHex); + } + + protected void VerifyWrite_DER_String_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + Asn1Tag tag = new Asn1Tag(TagClass.Application, 19, isConstructed: true); + Asn1Tag expected = new Asn1Tag(tag.TagClass, tag.TagValue); + WriteString(writer, tag, input); + + Verify(writer, Stringify(expected) + expectedPayloadHex); + } + + protected void VerifyWrite_DER_Span_ClearsConstructed(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + Asn1Tag standard = StandardTag; + Asn1Tag tag = new Asn1Tag(standard.TagClass, standard.TagValue, isConstructed: true); + WriteSpan(writer, tag, input.AsReadOnlySpan()); + + Verify(writer, Stringify(standard) + expectedPayloadHex); + } + + protected void VerifyWrite_DER_Span_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + Asn1Tag tag = new Asn1Tag(TagClass.Private, 24601, isConstructed: true); + Asn1Tag expected = new Asn1Tag(tag.TagClass, tag.TagValue); + WriteSpan(writer, tag, input.AsReadOnlySpan()); + + Verify(writer, Stringify(expected) + expectedPayloadHex); + } + + protected void VerifyWrite_String_Null(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentNullException>( + "str", + () => WriteString(writer, null)); + + } + + protected void VerifyWrite_String_Null_CustomTag(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentNullException>( + "str", + () => WriteString(writer, new Asn1Tag(TagClass.ContextSpecific, 3), null)); + } + + protected void VerifyWrite_EndOfContents_String(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => WriteString(writer, Asn1Tag.EndOfContents, "hi")); + } + + protected void VerifyWrite_EndOfContents_Span(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => WriteSpan(writer, Asn1Tag.EndOfContents, "hi".AsReadOnlySpan())); + } + + private void VerifyWrite_CERSegmented(AsnWriter writer, string tagHex, int contentByteCount) + { + int div = Math.DivRem(contentByteCount, 1000, out int rem); + + // tag, length(80), div full segments at 1004 bytes each, and the end of contents. + int encodedSize = (tagHex.Length / 2) + 1 + 1004 * div + 2; + + if (rem != 0) + { + // tag, contents (length TBD) + encodedSize += 1 + rem; + + if (encodedSize < 0x80) + encodedSize++; + else if (encodedSize <= 0xFF) + encodedSize += 2; + else + encodedSize += 3; + } + + byte[] encoded = writer.Encode(); + + Assert.Equal(tagHex, encoded.AsReadOnlySpan().Slice(0, tagHex.Length / 2).ByteArrayToHex()); + Assert.Equal("0000", encoded.AsReadOnlySpan().Slice(encoded.Length - 2).ByteArrayToHex()); + Assert.Equal(encodedSize, encoded.Length); + } + + protected void VerifyWrite_CERSegmented_String(string input, int contentByteCount) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + + Asn1Tag standard = StandardTag; + Asn1Tag tag = new Asn1Tag(standard.TagClass, standard.TagValue, true); + string tagHex = Stringify(tag); + + WriteString(writer, input); + VerifyWrite_CERSegmented(writer, tagHex, contentByteCount); + } + + protected void VerifyWrite_CERSegmented_String_CustomTag(string input, int contentByteCount) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + + Asn1Tag tag = new Asn1Tag(TagClass.Private, 7, true); + string tagHex = Stringify(tag); + + WriteString(writer, tag, input); + VerifyWrite_CERSegmented(writer, tagHex, contentByteCount); + } + + protected void VerifyWrite_CERSegmented_String_ConstructedTag(string input, int contentByteCount) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + + Asn1Tag standard = StandardTag; + Asn1Tag tag = new Asn1Tag(standard.TagClass, standard.TagValue, true); + string tagHex = Stringify(tag); + + WriteString(writer, tag, input); + VerifyWrite_CERSegmented(writer, tagHex, contentByteCount); + } + + protected void VerifyWrite_CERSegmented_String_CustomPrimitiveTag(string input, int contentByteCount) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + + Asn1Tag prim = new Asn1Tag(TagClass.Application, 42); + Asn1Tag constr = new Asn1Tag(prim.TagClass, prim.TagValue, true); + string tagHex = Stringify(constr); + + WriteString(writer, prim, input); + VerifyWrite_CERSegmented(writer, tagHex, contentByteCount); + } + + protected void VerifyWrite_CERSegmented_Span(string input, int contentByteCount) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + + Asn1Tag standard = StandardTag; + Asn1Tag tag = new Asn1Tag(standard.TagClass, standard.TagValue, true); + string tagHex = Stringify(tag); + + WriteSpan(writer, input.AsReadOnlySpan()); + VerifyWrite_CERSegmented(writer, tagHex, contentByteCount); + } + + protected void VerifyWrite_CERSegmented_Span_CustomTag(string input, int contentByteCount) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + + Asn1Tag tag = new Asn1Tag(TagClass.Private, 7, true); + string tagHex = Stringify(tag); + + WriteSpan(writer, tag, input.AsReadOnlySpan()); + VerifyWrite_CERSegmented(writer, tagHex, contentByteCount); + } + + protected void VerifyWrite_CERSegmented_Span_ConstructedTag(string input, int contentByteCount) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + + Asn1Tag standard = StandardTag; + Asn1Tag tag = new Asn1Tag(standard.TagClass, standard.TagValue, true); + string tagHex = Stringify(tag); + + WriteSpan(writer, tag, input.AsReadOnlySpan()); + VerifyWrite_CERSegmented(writer, tagHex, contentByteCount); + } + + protected void VerifyWrite_CERSegmented_Span_CustomPrimitiveTag(string input, int contentByteCount) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + + Asn1Tag prim = new Asn1Tag(TagClass.Application, 42); + Asn1Tag constr = new Asn1Tag(prim.TagClass, prim.TagValue, true); + string tagHex = Stringify(constr); + + WriteSpan(writer, prim, input.AsReadOnlySpan()); + VerifyWrite_CERSegmented(writer, tagHex, contentByteCount); + } + + protected void VerifyWrite_String_NonEncodable(string input) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + + Assert.Throws<EncoderFallbackException>(() => WriteString(writer, input)); + } + + protected void VerifyWrite_Span_NonEncodable(string input) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + + Assert.Throws<EncoderFallbackException>(() => WriteSpan(writer, input.AsReadOnlySpan())); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteEnumerated.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteEnumerated.cs new file mode 100644 index 0000000000..dead5a4a42 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteEnumerated.cs @@ -0,0 +1,402 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.X509Certificates; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteEnumerated : Asn1WriterTests + { + [Theory] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.SByteBacked.Zero, false, "0A0100")] + [InlineData(PublicEncodingRules.CER, ReadEnumerated.SByteBacked.Pillow, true, "9E01EF")] + [InlineData(PublicEncodingRules.DER, ReadEnumerated.SByteBacked.Fluff, false, "0A0153")] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.SByteBacked.Fluff, true, "9E0153")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.SByteBacked)(-127), true, "9E0181")] + public void VerifyWriteEnumerated_SByte( + PublicEncodingRules ruleSet, + ReadEnumerated.SByteBacked value, + bool customTag, + string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + if (customTag) + { + writer.WriteEnumeratedValue(new Asn1Tag(TagClass.ContextSpecific, 30), value); + } + else + { + writer.WriteEnumeratedValue(value); + } + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.ByteBacked.Zero, false, "0A0100")] + [InlineData(PublicEncodingRules.CER, ReadEnumerated.ByteBacked.NotFluffy, true, "9A010B")] + [InlineData(PublicEncodingRules.DER, ReadEnumerated.ByteBacked.Fluff, false, "0A010C")] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.ByteBacked.Fluff, true, "9A010C")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.ByteBacked)253, false, "0A0200FD")] + public void VerifyWriteEnumerated_Byte( + PublicEncodingRules ruleSet, + ReadEnumerated.ByteBacked value, + bool customTag, + string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + if (customTag) + { + writer.WriteEnumeratedValue(new Asn1Tag(TagClass.ContextSpecific, 26), value); + } + else + { + writer.WriteEnumeratedValue(value); + } + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.ShortBacked.Zero, true, "DF81540100")] + [InlineData(PublicEncodingRules.CER, ReadEnumerated.ShortBacked.Pillow, true, "DF815402FC00")] + [InlineData(PublicEncodingRules.DER, ReadEnumerated.ShortBacked.Fluff, false, "0A020209")] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.ShortBacked.Fluff, true, "DF8154020209")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.ShortBacked)25321, false, "0A0262E9")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.ShortBacked)(-12345), false, "0A02CFC7")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.ShortBacked)(-1), true, "DF815401FF")] + public void VerifyWriteEnumerated_Short( + PublicEncodingRules ruleSet, + ReadEnumerated.ShortBacked value, + bool customTag, + string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + if (customTag) + { + writer.WriteEnumeratedValue(new Asn1Tag(TagClass.Private, 212), value); + } + else + { + writer.WriteEnumeratedValue(value); + } + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.UShortBacked.Zero, false, "0A0100")] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.UShortBacked.Zero, true, "4D0100")] + [InlineData(PublicEncodingRules.DER, ReadEnumerated.UShortBacked.Fluff, false, "0A03008000")] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.UShortBacked.Fluff, true, "4D03008000")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.UShortBacked)11, false, "0A010B")] + [InlineData(PublicEncodingRules.DER, (ReadEnumerated.UShortBacked)short.MaxValue, false, "0A027FFF")] + [InlineData(PublicEncodingRules.BER, (ReadEnumerated.UShortBacked)ushort.MaxValue, true, "4D0300FFFF")] + public void VerifyWriteEnumerated_UShort( + PublicEncodingRules ruleSet, + ReadEnumerated.UShortBacked value, + bool customTag, + string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + if (customTag) + { + writer.WriteEnumeratedValue(new Asn1Tag(TagClass.Application, 13), value); + } + else + { + writer.WriteEnumeratedValue(value); + } + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.IntBacked.Zero, true, "5F81FF7F0100")] + [InlineData(PublicEncodingRules.CER, ReadEnumerated.IntBacked.Pillow, true, "5F81FF7F03FEFFFF")] + [InlineData(PublicEncodingRules.DER, ReadEnumerated.IntBacked.Fluff, false, "0A03010001")] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.IntBacked.Fluff, true, "5F81FF7F03010001")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.IntBacked)25321, false, "0A0262E9")] + [InlineData(PublicEncodingRules.DER, (ReadEnumerated.IntBacked)(-12345), false, "0A02CFC7")] + [InlineData(PublicEncodingRules.BER, (ReadEnumerated.IntBacked)(-1), true, "5F81FF7F01FF")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.IntBacked)int.MinValue, true, "5F81FF7F0480000000")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.IntBacked)int.MaxValue, false, "0A047FFFFFFF")] + public void VerifyWriteEnumerated_Int( + PublicEncodingRules ruleSet, + ReadEnumerated.IntBacked value, + bool customTag, + string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + if (customTag) + { + writer.WriteEnumeratedValue(new Asn1Tag(TagClass.Application, short.MaxValue), value); + } + else + { + writer.WriteEnumeratedValue(value); + } + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.UIntBacked.Zero, false, "0A0100")] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.UIntBacked.Zero, true, "9F610100")] + [InlineData(PublicEncodingRules.DER, ReadEnumerated.UIntBacked.Fluff, false, "0A050080000005")] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.UIntBacked.Fluff, true, "9F61050080000005")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.UIntBacked)11, false, "0A010B")] + [InlineData(PublicEncodingRules.DER, (ReadEnumerated.UIntBacked)short.MaxValue, false, "0A027FFF")] + [InlineData(PublicEncodingRules.BER, (ReadEnumerated.UIntBacked)ushort.MaxValue, true, "9F610300FFFF")] + public void VerifyWriteEnumerated_UInt( + PublicEncodingRules ruleSet, + ReadEnumerated.UIntBacked value, + bool customTag, + string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + if (customTag) + { + writer.WriteEnumeratedValue(new Asn1Tag(TagClass.ContextSpecific, 97), value); + } + else + { + writer.WriteEnumeratedValue(value); + } + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.LongBacked.Zero, true, "800100")] + [InlineData(PublicEncodingRules.CER, ReadEnumerated.LongBacked.Pillow, true, "8005FF00000000")] + [InlineData(PublicEncodingRules.DER, ReadEnumerated.LongBacked.Fluff, false, "0A050200000441")] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.LongBacked.Fluff, true, "80050200000441")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.LongBacked)25321, false, "0A0262E9")] + [InlineData(PublicEncodingRules.DER, (ReadEnumerated.LongBacked)(-12345), false, "0A02CFC7")] + [InlineData(PublicEncodingRules.BER, (ReadEnumerated.LongBacked)(-1), true, "8001FF")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.LongBacked)int.MinValue, true, "800480000000")] + [InlineData(PublicEncodingRules.DER, (ReadEnumerated.LongBacked)int.MaxValue, false, "0A047FFFFFFF")] + [InlineData(PublicEncodingRules.BER, (ReadEnumerated.LongBacked)long.MinValue, false, "0A088000000000000000")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.LongBacked)long.MaxValue, true, "80087FFFFFFFFFFFFFFF")] + public void VerifyWriteEnumerated_Long( + PublicEncodingRules ruleSet, + ReadEnumerated.LongBacked value, + bool customTag, + string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + if (customTag) + { + writer.WriteEnumeratedValue(new Asn1Tag(TagClass.ContextSpecific, 0), value); + } + else + { + writer.WriteEnumeratedValue(value); + } + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.ULongBacked.Zero, false, "0A0100")] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.ULongBacked.Zero, true, "C10100")] + [InlineData(PublicEncodingRules.DER, ReadEnumerated.ULongBacked.Fluff, false, "0A0900FACEF00DCAFEBEEF")] + [InlineData(PublicEncodingRules.BER, ReadEnumerated.ULongBacked.Fluff, true, "C10900FACEF00DCAFEBEEF")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.ULongBacked)11, false, "0A010B")] + [InlineData(PublicEncodingRules.DER, (ReadEnumerated.ULongBacked)short.MaxValue, false, "0A027FFF")] + [InlineData(PublicEncodingRules.BER, (ReadEnumerated.ULongBacked)ushort.MaxValue, true, "C10300FFFF")] + [InlineData(PublicEncodingRules.CER, (ReadEnumerated.ULongBacked)long.MaxValue, true, "C1087FFFFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.DER, (ReadEnumerated.ULongBacked)ulong.MaxValue, false, "0A0900FFFFFFFFFFFFFFFF")] + public void VerifyWriteEnumerated_ULong( + PublicEncodingRules ruleSet, + ReadEnumerated.ULongBacked value, + bool customTag, + string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + if (customTag) + { + writer.WriteEnumeratedValue(new Asn1Tag(TagClass.Private, 1), value); + } + else + { + writer.WriteEnumeratedValue(value); + } + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyFlagsBased(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tEnum", + () => writer.WriteEnumeratedValue(OpenFlags.IncludeArchived)); + + AssertExtensions.Throws<ArgumentException>( + "tEnum", + () => writer.WriteEnumeratedValue( + new Asn1Tag(TagClass.ContextSpecific, 13), + OpenFlags.IncludeArchived)); + + AssertExtensions.Throws<ArgumentException>( + "tEnum", + () => writer.WriteEnumeratedValue((object)OpenFlags.IncludeArchived)); + + AssertExtensions.Throws<ArgumentException>( + "tEnum", + () => writer.WriteEnumeratedValue( + new Asn1Tag(TagClass.ContextSpecific, 13), + (object)OpenFlags.IncludeArchived)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyNonEnum(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + Assert.Throws<ArgumentException>( + () => writer.WriteEnumeratedValue(5)); + + Assert.Throws<ArgumentException>( + () => writer.WriteEnumeratedValue((object)"hi")); + + Assert.Throws<ArgumentException>( + () => writer.WriteEnumeratedValue((object)5)); + + Assert.Throws<ArgumentException>( + () => writer.WriteEnumeratedValue(new Asn1Tag(TagClass.ContextSpecific, 3), 5)); + + Assert.Throws<ArgumentException>( + () => writer.WriteEnumeratedValue(new Asn1Tag(TagClass.ContextSpecific, 3), (object)"hi")); + + Assert.Throws<ArgumentException>( + () => writer.WriteEnumeratedValue(new Asn1Tag(TagClass.ContextSpecific, 3), (object)5)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyEndOfContents(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteEnumeratedValue(Asn1Tag.EndOfContents, ReadEnumerated.IntBacked.Pillow)); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteEnumeratedValue(Asn1Tag.EndOfContents, (object)ReadEnumerated.IntBacked.Pillow)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void VerifyWriteEnumeratedValue_NonNull(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentNullException>( + "enumValue", + () => writer.WriteEnumeratedValue(null)); + + AssertExtensions.Throws<ArgumentNullException>( + "enumValue", + () => writer.WriteEnumeratedValue( + new Asn1Tag(TagClass.ContextSpecific, 1), + null)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void VerifyWriteEnumeratedValue_Object(PublicEncodingRules ruleSet) + { + AsnWriter objWriter = new AsnWriter((AsnEncodingRules)ruleSet); + AsnWriter genWriter = new AsnWriter((AsnEncodingRules)ruleSet); + + genWriter.WriteEnumeratedValue(ReadEnumerated.UIntBacked.Fluff); + objWriter.WriteEnumeratedValue((object)ReadEnumerated.UIntBacked.Fluff); + + genWriter.WriteEnumeratedValue(ReadEnumerated.SByteBacked.Fluff); + objWriter.WriteEnumeratedValue((object)ReadEnumerated.SByteBacked.Fluff); + + genWriter.WriteEnumeratedValue(ReadEnumerated.ULongBacked.Fluff); + objWriter.WriteEnumeratedValue((object)ReadEnumerated.ULongBacked.Fluff); + + Verify(objWriter, genWriter.Encode().ByteArrayToHex()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void VerifyWriteEnumeratedValue_Object_WithTag(PublicEncodingRules ruleSet) + { + AsnWriter objWriter = new AsnWriter((AsnEncodingRules)ruleSet); + AsnWriter genWriter = new AsnWriter((AsnEncodingRules)ruleSet); + + Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 52); + + genWriter.WriteEnumeratedValue(tag, ReadEnumerated.UIntBacked.Fluff); + objWriter.WriteEnumeratedValue(tag, (object)ReadEnumerated.UIntBacked.Fluff); + + tag = new Asn1Tag(TagClass.Private, 4); + + genWriter.WriteEnumeratedValue(tag, ReadEnumerated.SByteBacked.Fluff); + objWriter.WriteEnumeratedValue(tag, (object)ReadEnumerated.SByteBacked.Fluff); + + tag = new Asn1Tag(TagClass.Application, 75); + + genWriter.WriteEnumeratedValue(tag, ReadEnumerated.ULongBacked.Fluff); + objWriter.WriteEnumeratedValue(tag, (object)ReadEnumerated.ULongBacked.Fluff); + + Verify(objWriter, genWriter.Encode().ByteArrayToHex()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyWriteEnumeratedValue_ConstructedIgnored(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + writer.WriteEnumeratedValue( + new Asn1Tag(UniversalTagNumber.Enumerated, isConstructed: true), + ReadEnumerated.ULongBacked.Fluff); + + writer.WriteEnumeratedValue( + new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true), + (object)ReadEnumerated.SByteBacked.Fluff); + + Verify(writer, "0A0900FACEF00DCAFEBEEF" + "800153"); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteGeneralizedTime.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteGeneralizedTime.cs new file mode 100644 index 0000000000..29fd0b980b --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteGeneralizedTime.cs @@ -0,0 +1,244 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Security.Cryptography.Asn1; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteGeneralizedTime : Asn1WriterTests + { + public static IEnumerable<object[]> TestCases { get; } = new object[][] + { + new object[] + { + new DateTimeOffset(2017, 10, 16, 8, 24, 3, TimeSpan.FromHours(-7)), + false, + "0F32303137313031363135323430335A", + }, + new object[] + { + new DateTimeOffset(1817, 10, 16, 21, 24, 3, TimeSpan.FromHours(6)), + false, + "0F31383137313031363135323430335A", + }, + new object[] + { + new DateTimeOffset(3000, 1, 1, 0, 0, 0, TimeSpan.Zero), + false, + "0F33303030303130313030303030305A", + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 999, TimeSpan.Zero), + false, + "1331393939313233313233353935392E3939395A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 999, TimeSpan.Zero), + true, + "0F31393939313233313233353935395A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 880, TimeSpan.Zero), + false, + "1231393939313233313233353935392E38385A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 880, TimeSpan.Zero), + true, + "0F31393939313233313233353935395A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 700, TimeSpan.Zero), + false, + "1131393939313233313233353935392E375A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 700, TimeSpan.Zero), + true, + "0F31393939313233313233353935395A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 123, TimeSpan.Zero) + TimeSpan.FromTicks(4567), + false, + "1731393939313233313233353935392E313233343536375A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 123, TimeSpan.Zero) + TimeSpan.FromTicks(4567), + true, + "0F31393939313233313233353935395A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 12, TimeSpan.Zero) + TimeSpan.FromTicks(3450), + false, + "1631393939313233313233353935392E3031323334355A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 12, TimeSpan.Zero) + TimeSpan.FromTicks(3450), + true, + "0F31393939313233313233353935395A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 1, TimeSpan.Zero) + TimeSpan.FromTicks(2300), + false, + "1531393939313233313233353935392E30303132335A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 1, TimeSpan.Zero) + TimeSpan.FromTicks(2300), + true, + "0F31393939313233313233353935395A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 0, TimeSpan.Zero) + TimeSpan.FromTicks(1000), + false, + "1431393939313233313233353935392E303030315A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, 0, TimeSpan.Zero) + TimeSpan.FromTicks(1000), + true, + "0F31393939313233313233353935395A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, TimeSpan.Zero) + TimeSpan.FromTicks(1), + false, + "1731393939313233313233353935392E303030303030315A" + }, + new object[] + { + new DateTimeOffset(1999, 12, 31, 23, 59, 59, TimeSpan.Zero) + TimeSpan.FromTicks(1), + true, + "0F31393939313233313233353935395A" + }, + }; + + [Theory] + [MemberData(nameof(TestCases))] + public void VerifyWriteGeneralizedTime_BER( + DateTimeOffset input, + bool omitFractionalSeconds, + string expectedHexPayload) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + writer.WriteGeneralizedTime(input, omitFractionalSeconds); + + Verify(writer, "18" + expectedHexPayload); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void VerifyWriteGeneralizedTime_BER_CustomTag( + DateTimeOffset input, + bool omitFractionalSeconds, + string expectedHexPayload) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag tag = new Asn1Tag(TagClass.Application, 11); + writer.WriteGeneralizedTime(tag, input, omitFractionalSeconds); + + Verify(writer, Stringify(tag) + expectedHexPayload); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void VerifyWriteGeneralizedTime_CER( + DateTimeOffset input, + bool omitFractionalSeconds, + string expectedHexPayload) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + writer.WriteGeneralizedTime(input, omitFractionalSeconds); + + Verify(writer, "18" + expectedHexPayload); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void VerifyWriteGeneralizedTime_CER_CustomTag( + DateTimeOffset input, + bool omitFractionalSeconds, + string expectedHexPayload) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + Asn1Tag tag = new Asn1Tag(TagClass.Private, 95); + writer.WriteGeneralizedTime(tag, input, omitFractionalSeconds); + + Verify(writer, Stringify(tag) + expectedHexPayload); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void VerifyWriteGeneralizedTime_DER( + DateTimeOffset input, + bool omitFractionalSeconds, + string expectedHexPayload) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + writer.WriteGeneralizedTime(input, omitFractionalSeconds); + + Verify(writer, "18" + expectedHexPayload); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void VerifyWriteGeneralizedTime_DER_CustomTag( + DateTimeOffset input, + bool omitFractionalSeconds, + string expectedHexPayload) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 3); + writer.WriteGeneralizedTime(tag, input, omitFractionalSeconds); + + Verify(writer, Stringify(tag) + expectedHexPayload); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, false)] + [InlineData(PublicEncodingRules.CER, false)] + [InlineData(PublicEncodingRules.DER, false)] + [InlineData(PublicEncodingRules.BER, true)] + [InlineData(PublicEncodingRules.CER, true)] + [InlineData(PublicEncodingRules.DER, true)] + public void VerifyWriteGeneralizedTime_EndOfContents( + PublicEncodingRules ruleSet, + bool omitFractionalSeconds) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteGeneralizedTime(Asn1Tag.EndOfContents, DateTimeOffset.Now, omitFractionalSeconds)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyWriteGeneralizedTime_IgnoresConstructed(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + DateTimeOffset value = new DateTimeOffset(2017, 11, 16, 17, 35, 1, TimeSpan.Zero); + + writer.WriteGeneralizedTime(new Asn1Tag(UniversalTagNumber.GeneralizedTime, true), value); + writer.WriteGeneralizedTime(new Asn1Tag(TagClass.ContextSpecific, 3, true), value); + Verify(writer, "180F32303137313131363137333530315A" + "830F32303137313131363137333530315A"); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteIA5String.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteIA5String.cs new file mode 100644 index 0000000000..8bcda287a5 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteIA5String.cs @@ -0,0 +1,288 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Security.Cryptography.Asn1; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteIA5String : WriteCharacterString + { + public static IEnumerable<object[]> ShortValidCases { get; } = new object[][] + { + new object[] + { + string.Empty, + "00", + }, + new object[] + { + "hi", + "026869", + }, + new object[] + { + "Dr. & Mrs. Smith-Jones & children", + "2144722E2026204D72732E20536D6974682D4A6F6E65732026206368696C6472656E", + }, + }; + + public static IEnumerable<object[]> LongValidCases { get; } = new object[][] + { + new object[] + { + new string('f', 957) + new string('w', 182), + "820473" + new string('6', 957 * 2) + new string('7', 182 * 2), + }, + }; + + public static IEnumerable<object[]> CERSegmentedCases { get; } = new object[][] + { + new object[] + { + GettysburgAddress, + 1458, + }, + new object[] + { + new string ('Q', 2000), + 2000, + }, + }; + + public static IEnumerable<object[]> InvalidInputs { get; } = new object[][] + { + new object[] { "Dr. & Mrs. Smith\u2010Jones \uFE60 children", }, + }; + + internal override void WriteString(AsnWriter writer, string s) => + writer.WriteCharacterString(UniversalTagNumber.IA5String, s); + + internal override void WriteString(AsnWriter writer, Asn1Tag tag, string s) => + writer.WriteCharacterString(tag, UniversalTagNumber.IA5String, s); + + internal override void WriteSpan(AsnWriter writer, ReadOnlySpan<char> s) => + writer.WriteCharacterString(UniversalTagNumber.IA5String, s); + + internal override void WriteSpan(AsnWriter writer, Asn1Tag tag, ReadOnlySpan<char> s) => + writer.WriteCharacterString(tag, UniversalTagNumber.IA5String, s); + + internal override Asn1Tag StandardTag => new Asn1Tag(UniversalTagNumber.IA5String); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_String(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_String(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_String_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_String_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_String(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_String(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_String_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_String_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_String(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_String(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_String_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_String_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_Span(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_Span(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_Span_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_Span_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_Span(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_Span(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_Span_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_Span_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_Span(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_Span(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_Span_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_Span_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_String_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_String_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_String_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_String_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_Span_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_Span_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_Span_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_Span_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_String_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_String_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_String_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_String_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_Span_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_Span_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_Span_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_Span_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_String_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_String_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_String_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_String_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_Span_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_Span_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_Span_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_Span_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public new void VerifyWrite_String_Null(PublicEncodingRules ruleSet) => + base.VerifyWrite_String_Null(ruleSet); + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public new void VerifyWrite_String_Null_CustomTag(PublicEncodingRules ruleSet) => + base.VerifyWrite_String_Null_CustomTag(ruleSet); + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public new void VerifyWrite_EndOfContents_String(PublicEncodingRules ruleSet) => + base.VerifyWrite_EndOfContents_String(ruleSet); + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public new void VerifyWrite_EndOfContents_Span(PublicEncodingRules ruleSet) => + base.VerifyWrite_EndOfContents_Span(ruleSet); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_String(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_String(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_String_CustomTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_String_CustomTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_String_ConstructedTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_String_ConstructedTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_String_CustomPrimitiveTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_String_CustomPrimitiveTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_Span(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_Span(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_Span_CustomTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_Span_CustomTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_Span_ConstructedTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_Span_ConstructedTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_Span_CustomPrimitiveTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_Span_CustomPrimitiveTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(InvalidInputs))] + public new void VerifyWrite_String_NonEncodable(string input) => + base.VerifyWrite_String_NonEncodable(input); + + [Theory] + [MemberData(nameof(InvalidInputs))] + public new void VerifyWrite_Span_NonEncodable(string input) => + base.VerifyWrite_Span_NonEncodable(input); + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteInteger.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteInteger.cs new file mode 100644 index 0000000000..fd06aa8710 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteInteger.cs @@ -0,0 +1,306 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; +using System.Security.Cryptography.Asn1; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteInteger : Asn1WriterTests + { + [Theory] + [InlineData(PublicEncodingRules.BER, 0, "020100")] + [InlineData(PublicEncodingRules.CER, 0, "020100")] + [InlineData(PublicEncodingRules.DER, 0, "020100")] + [InlineData(PublicEncodingRules.BER, -1, "0201FF")] + [InlineData(PublicEncodingRules.CER, -1, "0201FF")] + [InlineData(PublicEncodingRules.DER, -1, "0201FF")] + [InlineData(PublicEncodingRules.BER, -2, "0201FE")] + [InlineData(PublicEncodingRules.DER, sbyte.MinValue, "020180")] + [InlineData(PublicEncodingRules.BER, sbyte.MinValue + 1, "020181")] + [InlineData(PublicEncodingRules.CER, sbyte.MinValue - 1, "0202FF7F")] + [InlineData(PublicEncodingRules.DER, sbyte.MinValue - 2, "0202FF7E")] + [InlineData(PublicEncodingRules.BER, -256, "0202FF00")] + [InlineData(PublicEncodingRules.CER, -257, "0202FEFF")] + [InlineData(PublicEncodingRules.DER, short.MinValue, "02028000")] + [InlineData(PublicEncodingRules.BER, short.MinValue + 1, "02028001")] + [InlineData(PublicEncodingRules.CER, short.MinValue + byte.MaxValue, "020280FF")] + [InlineData(PublicEncodingRules.DER, short.MinValue - 1, "0203FF7FFF")] + [InlineData(PublicEncodingRules.BER, short.MinValue - 2, "0203FF7FFE")] + [InlineData(PublicEncodingRules.CER, -65281, "0203FF00FF")] + [InlineData(PublicEncodingRules.DER, -8388608, "0203800000")] + [InlineData(PublicEncodingRules.BER, -8388607, "0203800001")] + [InlineData(PublicEncodingRules.CER, -8388609, "0204FF7FFFFF")] + [InlineData(PublicEncodingRules.DER, -16777216, "0204FF000000")] + [InlineData(PublicEncodingRules.BER, -16777217, "0204FEFFFFFF")] + [InlineData(PublicEncodingRules.CER, int.MinValue, "020480000000")] + [InlineData(PublicEncodingRules.DER, int.MinValue + 1, "020480000001")] + [InlineData(PublicEncodingRules.BER, (long)int.MinValue - 1, "0205FF7FFFFFFF")] + [InlineData(PublicEncodingRules.CER, (long)int.MinValue - 2, "0205FF7FFFFFFE")] + [InlineData(PublicEncodingRules.DER, -4294967296, "0205FF00000000")] + [InlineData(PublicEncodingRules.BER, -4294967295, "0205FF00000001")] + [InlineData(PublicEncodingRules.CER, -4294967294, "0205FF00000002")] + [InlineData(PublicEncodingRules.DER, -4294967297, "0205FEFFFFFFFF")] + [InlineData(PublicEncodingRules.BER, -549755813888, "02058000000000")] + [InlineData(PublicEncodingRules.CER, -549755813887, "02058000000001")] + [InlineData(PublicEncodingRules.DER, -549755813889, "0206FF7FFFFFFFFF")] + [InlineData(PublicEncodingRules.BER, -549755813890, "0206FF7FFFFFFFFE")] + [InlineData(PublicEncodingRules.CER, -140737488355328, "0206800000000000")] + [InlineData(PublicEncodingRules.DER, -140737488355327, "0206800000000001")] + [InlineData(PublicEncodingRules.BER, -140737488355329, "0207FF7FFFFFFFFFFF")] + [InlineData(PublicEncodingRules.CER, -281474976710656, "0207FF000000000000")] + [InlineData(PublicEncodingRules.DER, -281474976710655, "0207FF000000000001")] + [InlineData(PublicEncodingRules.BER, -281474976710657, "0207FEFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.CER, -36028797018963968, "020780000000000000")] + [InlineData(PublicEncodingRules.DER, -36028797018963967, "020780000000000001")] + [InlineData(PublicEncodingRules.DER, -36028797018963969, "0208FF7FFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.BER, -36028797018963970, "0208FF7FFFFFFFFFFFFE")] + [InlineData(PublicEncodingRules.CER, -72057594037927936, "0208FF00000000000000")] + [InlineData(PublicEncodingRules.DER, -72057594037927935, "0208FF00000000000001")] + [InlineData(PublicEncodingRules.BER, -72057594037927937, "0208FEFFFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.CER, long.MinValue + 1, "02088000000000000001")] + [InlineData(PublicEncodingRules.DER, long.MinValue, "02088000000000000000")] + [InlineData(PublicEncodingRules.BER, 1, "020101")] + [InlineData(PublicEncodingRules.CER, 127, "02017F")] + [InlineData(PublicEncodingRules.DER, 126, "02017E")] + [InlineData(PublicEncodingRules.BER, 128, "02020080")] + [InlineData(PublicEncodingRules.CER, 129, "02020081")] + [InlineData(PublicEncodingRules.DER, 254, "020200FE")] + [InlineData(PublicEncodingRules.BER, 255, "020200FF")] + [InlineData(PublicEncodingRules.CER, 256, "02020100")] + [InlineData(PublicEncodingRules.DER, 32767, "02027FFF")] + [InlineData(PublicEncodingRules.BER, 32766, "02027FFE")] + [InlineData(PublicEncodingRules.CER, 32768, "0203008000")] + [InlineData(PublicEncodingRules.DER, 32769, "0203008001")] + [InlineData(PublicEncodingRules.BER, 65535, "020300FFFF")] + [InlineData(PublicEncodingRules.CER, 65534, "020300FFFE")] + [InlineData(PublicEncodingRules.DER, 65536, "0203010000")] + [InlineData(PublicEncodingRules.BER, 65537, "0203010001")] + [InlineData(PublicEncodingRules.CER, 8388607, "02037FFFFF")] + [InlineData(PublicEncodingRules.DER, 8388606, "02037FFFFE")] + [InlineData(PublicEncodingRules.BER, 8388608, "020400800000")] + [InlineData(PublicEncodingRules.CER, 8388609, "020400800001")] + [InlineData(PublicEncodingRules.DER, 16777215, "020400FFFFFF")] + [InlineData(PublicEncodingRules.BER, 16777214, "020400FFFFFE")] + [InlineData(PublicEncodingRules.CER, 16777216, "020401000000")] + [InlineData(PublicEncodingRules.DER, 16777217, "020401000001")] + [InlineData(PublicEncodingRules.BER, 2147483647, "02047FFFFFFF")] + [InlineData(PublicEncodingRules.CER, 2147483646, "02047FFFFFFE")] + [InlineData(PublicEncodingRules.DER, 2147483648, "02050080000000")] + [InlineData(PublicEncodingRules.BER, 2147483649, "02050080000001")] + [InlineData(PublicEncodingRules.BER, 4294967295, "020500FFFFFFFF")] + [InlineData(PublicEncodingRules.CER, 4294967294, "020500FFFFFFFE")] + [InlineData(PublicEncodingRules.DER, 4294967296, "02050100000000")] + [InlineData(PublicEncodingRules.BER, 4294967297, "02050100000001")] + [InlineData(PublicEncodingRules.CER, 549755813887, "02057FFFFFFFFF")] + [InlineData(PublicEncodingRules.DER, 549755813886, "02057FFFFFFFFE")] + [InlineData(PublicEncodingRules.BER, 549755813888, "0206008000000000")] + [InlineData(PublicEncodingRules.CER, 549755813889, "0206008000000001")] + [InlineData(PublicEncodingRules.DER, 1099511627775, "020600FFFFFFFFFF")] + [InlineData(PublicEncodingRules.BER, 1099511627774, "020600FFFFFFFFFE")] + [InlineData(PublicEncodingRules.CER, 1099511627776, "0206010000000000")] + [InlineData(PublicEncodingRules.DER, 1099511627777, "0206010000000001")] + [InlineData(PublicEncodingRules.BER, 140737488355327, "02067FFFFFFFFFFF")] + [InlineData(PublicEncodingRules.CER, 140737488355326, "02067FFFFFFFFFFE")] + [InlineData(PublicEncodingRules.DER, 140737488355328, "020700800000000000")] + [InlineData(PublicEncodingRules.BER, 140737488355329, "020700800000000001")] + [InlineData(PublicEncodingRules.CER, 281474976710655, "020700FFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.DER, 281474976710654, "020700FFFFFFFFFFFE")] + [InlineData(PublicEncodingRules.BER, 281474976710656, "020701000000000000")] + [InlineData(PublicEncodingRules.CER, 281474976710657, "020701000000000001")] + [InlineData(PublicEncodingRules.DER, 36028797018963967, "02077FFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.BER, 36028797018963966, "02077FFFFFFFFFFFFE")] + [InlineData(PublicEncodingRules.CER, 36028797018963968, "02080080000000000000")] + [InlineData(PublicEncodingRules.DER, 36028797018963969, "02080080000000000001")] + [InlineData(PublicEncodingRules.BER, 72057594037927935, "020800FFFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.CER, 72057594037927934, "020800FFFFFFFFFFFFFE")] + [InlineData(PublicEncodingRules.DER, 72057594037927936, "02080100000000000000")] + [InlineData(PublicEncodingRules.BER, 72057594037927937, "02080100000000000001")] + [InlineData(PublicEncodingRules.CER, 9223372036854775807, "02087FFFFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.DER, 9223372036854775806, "02087FFFFFFFFFFFFFFE")] + public void VerifyWriteInteger_Long(PublicEncodingRules ruleSet, long value, string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteInteger(value); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, 0, "020100")] + [InlineData(PublicEncodingRules.CER, 0, "020100")] + [InlineData(PublicEncodingRules.DER, 0, "020100")] + [InlineData(PublicEncodingRules.BER, 1, "020101")] + [InlineData(PublicEncodingRules.CER, 127, "02017F")] + [InlineData(PublicEncodingRules.DER, 126, "02017E")] + [InlineData(PublicEncodingRules.BER, 128, "02020080")] + [InlineData(PublicEncodingRules.CER, 129, "02020081")] + [InlineData(PublicEncodingRules.DER, 254, "020200FE")] + [InlineData(PublicEncodingRules.BER, 255, "020200FF")] + [InlineData(PublicEncodingRules.CER, 256, "02020100")] + [InlineData(PublicEncodingRules.DER, 32767, "02027FFF")] + [InlineData(PublicEncodingRules.BER, 32766, "02027FFE")] + [InlineData(PublicEncodingRules.CER, 32768, "0203008000")] + [InlineData(PublicEncodingRules.DER, 32769, "0203008001")] + [InlineData(PublicEncodingRules.BER, 65535, "020300FFFF")] + [InlineData(PublicEncodingRules.CER, 65534, "020300FFFE")] + [InlineData(PublicEncodingRules.DER, 65536, "0203010000")] + [InlineData(PublicEncodingRules.BER, 65537, "0203010001")] + [InlineData(PublicEncodingRules.CER, 8388607, "02037FFFFF")] + [InlineData(PublicEncodingRules.DER, 8388606, "02037FFFFE")] + [InlineData(PublicEncodingRules.BER, 8388608, "020400800000")] + [InlineData(PublicEncodingRules.CER, 8388609, "020400800001")] + [InlineData(PublicEncodingRules.DER, 16777215, "020400FFFFFF")] + [InlineData(PublicEncodingRules.BER, 16777214, "020400FFFFFE")] + [InlineData(PublicEncodingRules.CER, 16777216, "020401000000")] + [InlineData(PublicEncodingRules.DER, 16777217, "020401000001")] + [InlineData(PublicEncodingRules.BER, 2147483647, "02047FFFFFFF")] + [InlineData(PublicEncodingRules.CER, 2147483646, "02047FFFFFFE")] + [InlineData(PublicEncodingRules.DER, 2147483648, "02050080000000")] + [InlineData(PublicEncodingRules.BER, 2147483649, "02050080000001")] + [InlineData(PublicEncodingRules.BER, 4294967295, "020500FFFFFFFF")] + [InlineData(PublicEncodingRules.CER, 4294967294, "020500FFFFFFFE")] + [InlineData(PublicEncodingRules.DER, 4294967296, "02050100000000")] + [InlineData(PublicEncodingRules.BER, 4294967297, "02050100000001")] + [InlineData(PublicEncodingRules.CER, 549755813887, "02057FFFFFFFFF")] + [InlineData(PublicEncodingRules.DER, 549755813886, "02057FFFFFFFFE")] + [InlineData(PublicEncodingRules.BER, 549755813888, "0206008000000000")] + [InlineData(PublicEncodingRules.CER, 549755813889, "0206008000000001")] + [InlineData(PublicEncodingRules.DER, 1099511627775, "020600FFFFFFFFFF")] + [InlineData(PublicEncodingRules.BER, 1099511627774, "020600FFFFFFFFFE")] + [InlineData(PublicEncodingRules.CER, 1099511627776, "0206010000000000")] + [InlineData(PublicEncodingRules.DER, 1099511627777, "0206010000000001")] + [InlineData(PublicEncodingRules.BER, 140737488355327, "02067FFFFFFFFFFF")] + [InlineData(PublicEncodingRules.CER, 140737488355326, "02067FFFFFFFFFFE")] + [InlineData(PublicEncodingRules.DER, 140737488355328, "020700800000000000")] + [InlineData(PublicEncodingRules.BER, 140737488355329, "020700800000000001")] + [InlineData(PublicEncodingRules.CER, 281474976710655, "020700FFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.DER, 281474976710654, "020700FFFFFFFFFFFE")] + [InlineData(PublicEncodingRules.BER, 281474976710656, "020701000000000000")] + [InlineData(PublicEncodingRules.CER, 281474976710657, "020701000000000001")] + [InlineData(PublicEncodingRules.DER, 36028797018963967, "02077FFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.BER, 36028797018963966, "02077FFFFFFFFFFFFE")] + [InlineData(PublicEncodingRules.CER, 36028797018963968, "02080080000000000000")] + [InlineData(PublicEncodingRules.DER, 36028797018963969, "02080080000000000001")] + [InlineData(PublicEncodingRules.BER, 72057594037927935, "020800FFFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.CER, 72057594037927934, "020800FFFFFFFFFFFFFE")] + [InlineData(PublicEncodingRules.DER, 72057594037927936, "02080100000000000000")] + [InlineData(PublicEncodingRules.BER, 72057594037927937, "02080100000000000001")] + [InlineData(PublicEncodingRules.CER, 9223372036854775807, "02087FFFFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.DER, 9223372036854775806, "02087FFFFFFFFFFFFFFE")] + [InlineData(PublicEncodingRules.BER, 9223372036854775808, "0209008000000000000000")] + [InlineData(PublicEncodingRules.CER, 9223372036854775809, "0209008000000000000001")] + [InlineData(PublicEncodingRules.DER, ulong.MaxValue, "020900FFFFFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.BER, ulong.MaxValue-1, "020900FFFFFFFFFFFFFFFE")] + public void VerifyWriteInteger_ULong(PublicEncodingRules ruleSet, ulong value, string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteInteger(value); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, "0", "020100")] + [InlineData(PublicEncodingRules.CER, "127", "02017F")] + [InlineData(PublicEncodingRules.DER, "128", "02020080")] + [InlineData(PublicEncodingRules.BER, "32767", "02027FFF")] + [InlineData(PublicEncodingRules.CER, "32768", "0203008000")] + [InlineData(PublicEncodingRules.DER, "9223372036854775807", "02087FFFFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.BER, "9223372036854775808", "0209008000000000000000")] + [InlineData(PublicEncodingRules.CER, "18446744073709551615", "020900FFFFFFFFFFFFFFFF")] + [InlineData(PublicEncodingRules.DER, "18446744073709551616", "0209010000000000000000")] + [InlineData(PublicEncodingRules.BER, "1339673755198158349044581307228491520", "02100102030405060708090A0B0C0D0E0F00")] + [InlineData(PublicEncodingRules.CER, "320182027492359845421654932427609477120", "021100F0E0D0C0B0A090807060504030201000")] + [InlineData(PublicEncodingRules.DER, "-1339673755198158349044581307228491520", "0210FEFDFCFBFAF9F8F7F6F5F4F3F2F1F100")] + public void VerifyWriteInteger_BigInteger(PublicEncodingRules ruleSet, string decimalValue, string expectedHex) + { + BigInteger value = BigInteger.Parse(decimalValue); + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteInteger(value); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, 0, "470100")] + [InlineData(PublicEncodingRules.CER, long.MinValue + 1, "47088000000000000001")] + [InlineData(PublicEncodingRules.DER, 9223372036854775806, "47087FFFFFFFFFFFFFFE")] + public void VerifyWriteInteger_Application7_Long(PublicEncodingRules ruleSet, long value, string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteInteger(new Asn1Tag(TagClass.Application, 7), value); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, 0, "890100")] + [InlineData(PublicEncodingRules.CER, 9223372036854775809, "8909008000000000000001")] + [InlineData(PublicEncodingRules.DER, 9223372036854775806, "89087FFFFFFFFFFFFFFE")] + public void VerifyWriteInteger_Context9_ULong(PublicEncodingRules ruleSet, ulong value, string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteInteger(new Asn1Tag(TagClass.ContextSpecific, 9), value); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, 0, "D00100")] + [InlineData(PublicEncodingRules.BER, "1339673755198158349044581307228491520", "D0100102030405060708090A0B0C0D0E0F00")] + [InlineData(PublicEncodingRules.CER, "320182027492359845421654932427609477120", "D01100F0E0D0C0B0A090807060504030201000")] + [InlineData(PublicEncodingRules.DER, "-1339673755198158349044581307228491520", "D010FEFDFCFBFAF9F8F7F6F5F4F3F2F1F100")] + public void VerifyWriteInteger_Private16_BigInteger( + PublicEncodingRules ruleSet, + string decimalValue, + string expectedHex) + { + BigInteger value = BigInteger.Parse(decimalValue); + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteInteger(new Asn1Tag(TagClass.Private, 16), value); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyWriteInteger_EndOfContents(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteInteger(Asn1Tag.EndOfContents, 0L)); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteInteger(Asn1Tag.EndOfContents, 0UL)); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteInteger(Asn1Tag.EndOfContents, BigInteger.Zero)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyWriteInteger_ConstructedIgnored(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteInteger(new Asn1Tag(UniversalTagNumber.Integer, isConstructed: true), 0L); + writer.WriteInteger(new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true), 0L); + writer.WriteInteger(new Asn1Tag(UniversalTagNumber.Integer, isConstructed: true), 0UL); + writer.WriteInteger(new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true), 0UL); + writer.WriteInteger(new Asn1Tag(UniversalTagNumber.Integer, isConstructed: true), BigInteger.Zero); + writer.WriteInteger(new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true), BigInteger.Zero); + + Verify(writer, "020100800100020100800100020100800100"); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteNamedBitList.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteNamedBitList.cs new file mode 100644 index 0000000000..a30f752e1a --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteNamedBitList.cs @@ -0,0 +1,227 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteNamedBitList : Asn1WriterTests + { + [Theory] + [InlineData( + PublicEncodingRules.BER, + "030100", + ReadNamedBitList.ULongFlags.None)] + [InlineData( + PublicEncodingRules.CER, + "030100", + ReadNamedBitList.ULongFlags.None)] + [InlineData( + PublicEncodingRules.DER, + "030100", + ReadNamedBitList.ULongFlags.None)] + [InlineData( + PublicEncodingRules.BER, + "0309000000000000000003", + ReadNamedBitList.ULongFlags.Max | ReadNamedBitList.ULongFlags.AlmostMax)] + [InlineData( + PublicEncodingRules.CER, + "0309010000000080000002", + ReadNamedBitList.LongFlags.Max | ReadNamedBitList.LongFlags.Mid)] + [InlineData( + PublicEncodingRules.DER, + "030204B0", + ReadNamedBitList.X509KeyUsageCSharpStyle.DigitalSignature | + ReadNamedBitList.X509KeyUsageCSharpStyle.KeyEncipherment | + ReadNamedBitList.X509KeyUsageCSharpStyle.DataEncipherment)] + public static void VerifyWriteNamedBitList( + PublicEncodingRules ruleSet, + string expectedHex, + object value) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteNamedBitList(value); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData( + PublicEncodingRules.BER, + "C00100", + ReadNamedBitList.ULongFlags.None)] + [InlineData( + PublicEncodingRules.CER, + "410100", + ReadNamedBitList.ULongFlags.None)] + [InlineData( + PublicEncodingRules.DER, + "820100", + ReadNamedBitList.ULongFlags.None)] + [InlineData( + PublicEncodingRules.BER, + "C009000000000000000003", + ReadNamedBitList.ULongFlags.Max | ReadNamedBitList.ULongFlags.AlmostMax)] + [InlineData( + PublicEncodingRules.CER, + "4109010000000080000002", + ReadNamedBitList.LongFlags.Max | ReadNamedBitList.LongFlags.Mid)] + [InlineData( + PublicEncodingRules.DER, + "820204B0", + ReadNamedBitList.X509KeyUsageCSharpStyle.DigitalSignature | + ReadNamedBitList.X509KeyUsageCSharpStyle.KeyEncipherment | + ReadNamedBitList.X509KeyUsageCSharpStyle.DataEncipherment)] + public static void VerifyWriteNamedBitList_WithTag( + PublicEncodingRules ruleSet, + string expectedHex, + object value) + { + int ruleSetVal = (int)ruleSet; + TagClass tagClass = (TagClass)(byte)(ruleSetVal << 6); + + if (tagClass == TagClass.Universal) + tagClass = TagClass.Private; + + Asn1Tag tag = new Asn1Tag(tagClass, ruleSetVal); + + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteNamedBitList(tag, value); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void VerifyWriteNamedBitList_Generic(PublicEncodingRules ruleSet) + { + AsnWriter objWriter = new AsnWriter((AsnEncodingRules)ruleSet); + AsnWriter genWriter = new AsnWriter((AsnEncodingRules)ruleSet); + + var flagsValue = + ReadNamedBitList.X509KeyUsageCSharpStyle.DigitalSignature | + ReadNamedBitList.X509KeyUsageCSharpStyle.KeyEncipherment | + ReadNamedBitList.X509KeyUsageCSharpStyle.DataEncipherment; + + genWriter.WriteNamedBitList(flagsValue); + objWriter.WriteNamedBitList((object)flagsValue); + + Verify(genWriter, objWriter.Encode().ByteArrayToHex()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void VerifyWriteNamedBitList_Generic_WithTag(PublicEncodingRules ruleSet) + { + AsnWriter objWriter = new AsnWriter((AsnEncodingRules)ruleSet); + AsnWriter genWriter = new AsnWriter((AsnEncodingRules)ruleSet); + + Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 52); + + var flagsValue = + ReadNamedBitList.X509KeyUsageCSharpStyle.DigitalSignature | + ReadNamedBitList.X509KeyUsageCSharpStyle.KeyEncipherment | + ReadNamedBitList.X509KeyUsageCSharpStyle.DataEncipherment; + + genWriter.WriteNamedBitList(tag, flagsValue); + objWriter.WriteNamedBitList(tag, (object)flagsValue); + + Verify(genWriter, objWriter.Encode().ByteArrayToHex()); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void VerifyWriteNamedBitList_NonNull(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentNullException>( + "enumValue", + () => writer.WriteNamedBitList(null)); + + AssertExtensions.Throws<ArgumentNullException>( + "enumValue", + () => writer.WriteNamedBitList(new Asn1Tag(TagClass.ContextSpecific, 1), null)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void VerifyWriteNamedBitList_EnumRequired(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + Assert.Throws<ArgumentException>( + () => writer.WriteNamedBitList(3)); + + Assert.Throws<ArgumentException>( + () => writer.WriteNamedBitList(new Asn1Tag(TagClass.ContextSpecific, 1), 3)); + + Assert.Throws<ArgumentException>( + () => writer.WriteNamedBitList((object)3)); + + Assert.Throws<ArgumentException>( + () => writer.WriteNamedBitList(new Asn1Tag(TagClass.ContextSpecific, 1), (object)3)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void VerifyWriteNamedBitList_FlagsEnumRequired(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tEnum", + () => writer.WriteNamedBitList(AsnEncodingRules.BER)); + + AssertExtensions.Throws<ArgumentException>( + "tEnum", + () => writer.WriteNamedBitList( + new Asn1Tag(TagClass.ContextSpecific, 1), + AsnEncodingRules.BER)); + + AssertExtensions.Throws<ArgumentException>( + "tEnum", + () => writer.WriteNamedBitList((object)AsnEncodingRules.BER)); + + AssertExtensions.Throws<ArgumentException>( + "tEnum", + () => writer.WriteNamedBitList( + new Asn1Tag(TagClass.ContextSpecific, 1), + (object)AsnEncodingRules.BER)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public static void VerifyWriteNamedBitList_EndOfContents(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteNamedBitList( + Asn1Tag.EndOfContents, + StringSplitOptions.RemoveEmptyEntries)); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteNamedBitList( + Asn1Tag.EndOfContents, + (object)StringSplitOptions.RemoveEmptyEntries)); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteNull.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteNull.cs new file mode 100644 index 0000000000..dc1c7117b2 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteNull.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteNull : Asn1WriterTests + { + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.DER)] + [InlineData(PublicEncodingRules.CER)] + public void VerifyWriteNull(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteNull(); + + Verify(writer, "0500"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.DER)] + [InlineData(PublicEncodingRules.CER)] + public void VerifyWriteNull_EndOfContents(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteNull(Asn1Tag.EndOfContents)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.DER)] + [InlineData(PublicEncodingRules.CER)] + public void VerifyWriteNull_ConstructedIgnored(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteNull(new Asn1Tag(TagClass.ContextSpecific, 7, true)); + writer.WriteNull(new Asn1Tag(UniversalTagNumber.Null, true)); + + Verify(writer, "87000500"); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteObjectIdentifier.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteObjectIdentifier.cs new file mode 100644 index 0000000000..ca568825b8 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteObjectIdentifier.cs @@ -0,0 +1,289 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Security.Cryptography.Asn1; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteObjectIdentifier : Asn1WriterTests + { + [Theory] + [MemberData(nameof(ValidOidData))] + public void VerifyWriteObjectIdentifier_String( + PublicEncodingRules ruleSet, + string oidValue, + string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteObjectIdentifier(oidValue); + + Verify(writer, expectedHex); + } + + [Theory] + [MemberData(nameof(ValidOidData))] + public void VerifyWriteObjectIdentifier_Span( + PublicEncodingRules ruleSet, + string oidValue, + string expectedHex) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteObjectIdentifier(oidValue.AsReadOnlySpan()); + + Verify(writer, expectedHex); + } + + [Theory] + [MemberData(nameof(ValidOidData))] + public void VerifyWriteObjectIdentifier_Oid( + PublicEncodingRules ruleSet, + string oidValue, + string expectedHex) + { + Oid oidObj = new Oid(oidValue, "FriendlyName does not matter"); + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteObjectIdentifier(oidObj); + + Verify(writer, expectedHex); + } + + [Theory] + [MemberData(nameof(InvalidOidData))] + public void VerifyWriteOid_InvalidValue_String( + string description, + PublicEncodingRules ruleSet, + string nonOidValue) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => writer.WriteObjectIdentifier(nonOidValue)); + } + + [Theory] + [MemberData(nameof(InvalidOidData))] + public void VerifyWriteOid_InvalidValue_Span( + string description, + PublicEncodingRules ruleSet, + string nonOidValue) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + Assert.Throws<CryptographicException>( + () => writer.WriteObjectIdentifier(nonOidValue.AsReadOnlySpan())); + } + + [Theory] + [MemberData(nameof(InvalidOidData))] + public void VerifyWriteOid_InvalidValue_Oid( + string description, + PublicEncodingRules ruleSet, + string nonOidValue) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + Oid nonOidObj = new Oid(nonOidValue, "FriendlyName does not matter"); + + Assert.Throws<CryptographicException>( + () => writer.WriteObjectIdentifier(nonOidObj)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void WriteObjectIdentifier_CustomTag_String(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteObjectIdentifier(new Asn1Tag(TagClass.ContextSpecific, 3), "1.3.14.3.2.26"); + + Verify(writer, "83052B0E03021A"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void WriteObjectIdentifier_CustomTag_Span(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteObjectIdentifier(new Asn1Tag(TagClass.Application, 2), "1.3.14.3.2.26".AsReadOnlySpan()); + + Verify(writer, "42052B0E03021A"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void WriteObjectIdentifier_CustomTag_Oid(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteObjectIdentifier(new Asn1Tag(TagClass.Private, 36), Oid.FromFriendlyName("SHA1", OidGroup.HashAlgorithm)); + + Verify(writer, "DF24052B0E03021A"); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WriteObjectIdentifier_NullString(bool defaultTag) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + + AssertExtensions.Throws<ArgumentNullException>( + "oidValue", + () => + { + if (defaultTag) + { + writer.WriteObjectIdentifier((string)null); + } + else + { + writer.WriteObjectIdentifier(new Asn1Tag(TagClass.ContextSpecific, 6), (string)null); + } + }); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WriteObjectIdentifier_NullOid(bool defaultTag) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + + AssertExtensions.Throws<ArgumentNullException>( + "oid", + () => + { + if (defaultTag) + { + writer.WriteObjectIdentifier((Oid)null); + } + else + { + writer.WriteObjectIdentifier(new Asn1Tag(TagClass.Application, 2), (Oid)null); + } + }); + } + + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyWriteObjectIdentifier_EndOfContents(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteObjectIdentifier(Asn1Tag.EndOfContents, "1.1")); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteObjectIdentifier(Asn1Tag.EndOfContents, "1.1".AsReadOnlySpan())); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteObjectIdentifier(Asn1Tag.EndOfContents, new Oid("1.1", "1.1"))); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyWriteObjectIdentifier_ConstructedIgnored(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + const string OidValue = "1.1"; + Asn1Tag constructedOid = new Asn1Tag(UniversalTagNumber.ObjectIdentifier, isConstructed: true); + Asn1Tag constructedContext0 = new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true); + + writer.WriteObjectIdentifier(constructedOid, OidValue); + writer.WriteObjectIdentifier(constructedContext0, OidValue); + writer.WriteObjectIdentifier(constructedOid, OidValue.AsReadOnlySpan()); + writer.WriteObjectIdentifier(constructedContext0, OidValue.AsReadOnlySpan()); + writer.WriteObjectIdentifier(constructedOid, new Oid(OidValue, OidValue)); + writer.WriteObjectIdentifier(constructedContext0, new Oid(OidValue, OidValue)); + + Verify(writer, "060129800129060129800129060129800129"); + } + + public static IEnumerable<object[]> ValidOidData { get; } = + new object[][] + { + new object[] + { + PublicEncodingRules.BER, + "0.0", + "060100", + }, + new object[] + { + PublicEncodingRules.CER, + "1.0", + "060128", + }, + new object[] + { + PublicEncodingRules.DER, + "2.0", + "060150", + }, + new object[] + { + PublicEncodingRules.BER, + "1.3.14.3.2.26", + "06052B0E03021A", + }, + new object[] + { + PublicEncodingRules.CER, + "2.999.19427512891.25", + "06088837C8AFE1A43B19", + }, + new object[] + { + PublicEncodingRules.DER, + "1.2.840.113549.1.1.10", + "06092A864886F70D01010A", + }, + new object[] + { + // Using the rules of ITU-T-REC-X.667-201210 for 2.25.{UUID} unregistered arcs, and + // their sample value of f81d4fae-7dec-11d0-a765-00a0c91e6bf6 + // this is + // { joint-iso-itu-t(2) uuid(255) thatuuid(329800735698586629295641978511506172918) three(3) } + PublicEncodingRules.DER, + "2.25.329800735698586629295641978511506172918.3", + "06156983F09DA7EBCFDEE0C7A1A7B2C0948CC8F9D77603", + }, + }; + + public static IEnumerable<object[]> InvalidOidData { get; } = + new object[][] + { + new object[] { "Empty string", PublicEncodingRules.BER, "" }, + new object[] { "No period", PublicEncodingRules.CER, "1" }, + new object[] { "No second RID", PublicEncodingRules.DER, "1." }, + new object[] { "Invalid first RID", PublicEncodingRules.BER, "3.0" }, + new object[] { "Invalid first RID - multichar", PublicEncodingRules.CER, "27.0" }, + new object[] { "Double zero - First RID", PublicEncodingRules.DER, "00.0" }, + new object[] { "Leading zero - First RID", PublicEncodingRules.BER, "01.0" }, + new object[] { "Double zero - second RID", PublicEncodingRules.CER, "0.00" }, + new object[] { "Leading zero - second RID", PublicEncodingRules.DER, "0.01" }, + new object[] { "Ends with period - second RID", PublicEncodingRules.BER, "0.0." }, + new object[] { "Ends with period - third RID", PublicEncodingRules.BER, "0.1.30." }, + new object[] { "Double zero - third RID", PublicEncodingRules.CER, "0.1.00" }, + new object[] { "Leading zero - third RID", PublicEncodingRules.DER, "0.1.023" }, + new object[] { "Invalid character first position", PublicEncodingRules.BER, "a.1.23" }, + new object[] { "Invalid character second position", PublicEncodingRules.CER, "0,1.23" }, + new object[] { "Invalid character second rid", PublicEncodingRules.DER, "0.1q.23" }, + new object[] { "Invalid character third rid", PublicEncodingRules.BER, "0.1.23q" }, + }; + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteOctetString.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteOctetString.cs new file mode 100644 index 0000000000..18825af8c4 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteOctetString.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.Asn1; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteOctetString : Asn1WriterTests + { + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void WriteEmpty(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteOctetString(ReadOnlySpan<byte>.Empty); + + Verify(writer, "0400"); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, 1, "0401")] + [InlineData(PublicEncodingRules.CER, 2, "0402")] + [InlineData(PublicEncodingRules.DER, 3, "0403")] + [InlineData(PublicEncodingRules.BER, 126, "047E")] + [InlineData(PublicEncodingRules.CER, 127, "047F")] + [InlineData(PublicEncodingRules.DER, 128, "048180")] + [InlineData(PublicEncodingRules.BER, 1000, "048203E8")] + [InlineData(PublicEncodingRules.CER, 1000, "048203E8")] + [InlineData(PublicEncodingRules.DER, 1000, "048203E8")] + [InlineData(PublicEncodingRules.BER, 1001, "048203E9")] + [InlineData(PublicEncodingRules.DER, 1001, "048203E9")] + [InlineData(PublicEncodingRules.BER, 2001, "048207D1")] + [InlineData(PublicEncodingRules.DER, 2001, "048207D1")] + public void WritePrimitive(PublicEncodingRules ruleSet, int length, string hexStart) + { + string payloadHex = new string('0', 2 * length); + string expectedHex = hexStart + payloadHex; + byte[] data = new byte[length]; + + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteOctetString(data); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(1001, "2480048203E8", "0401")] + [InlineData(1999, "2480048203E8", "048203E7")] + [InlineData(2000, "2480048203E8", "048203E8")] + public void WriteSegmentedCER(int length, string hexStart, string hexStart2) + { + string payload1Hex = new string('8', 2000); + string payload2Hex = new string('8', (length - 1000) * 2); + string expectedHex = hexStart + payload1Hex + hexStart2 + payload2Hex + "0000"; + byte[] data = new byte[length]; + + for (int i = 0; i < data.Length; i++) + { + data[i] = 0x88; + } + + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + writer.WriteOctetString(data); + + Verify(writer, expectedHex); + } + + [Theory] + [InlineData(PublicEncodingRules.BER, 0, false)] + [InlineData(PublicEncodingRules.CER, 0, false)] + [InlineData(PublicEncodingRules.DER, 0, false)] + [InlineData(PublicEncodingRules.BER, 999, false)] + [InlineData(PublicEncodingRules.CER, 999, false)] + [InlineData(PublicEncodingRules.DER, 999, false)] + [InlineData(PublicEncodingRules.BER, 1000, false)] + [InlineData(PublicEncodingRules.CER, 1000, false)] + [InlineData(PublicEncodingRules.DER, 1000, false)] + [InlineData(PublicEncodingRules.BER, 1001, false)] + [InlineData(PublicEncodingRules.CER, 1001, true)] + [InlineData(PublicEncodingRules.DER, 1001, false)] + [InlineData(PublicEncodingRules.BER, 1998, false)] + [InlineData(PublicEncodingRules.CER, 1998, true)] + [InlineData(PublicEncodingRules.DER, 1998, false)] + [InlineData(PublicEncodingRules.BER, 1999, false)] + [InlineData(PublicEncodingRules.CER, 1999, true)] + [InlineData(PublicEncodingRules.DER, 1999, false)] + [InlineData(PublicEncodingRules.BER, 2000, false)] + [InlineData(PublicEncodingRules.CER, 2000, true)] + [InlineData(PublicEncodingRules.DER, 2000, false)] + [InlineData(PublicEncodingRules.BER, 2001, false)] + [InlineData(PublicEncodingRules.CER, 2001, true)] + [InlineData(PublicEncodingRules.DER, 2001, false)] + [InlineData(PublicEncodingRules.BER, 4096, false)] + [InlineData(PublicEncodingRules.CER, 4096, true)] + [InlineData(PublicEncodingRules.DER, 4096, false)] + public void VerifyWriteOctetString_PrimitiveOrConstructed( + PublicEncodingRules ruleSet, + int payloadLength, + bool expectConstructed) + { + byte[] data = new byte[payloadLength]; + + Asn1Tag[] tagsToTry = + { + new Asn1Tag(UniversalTagNumber.OctetString), + new Asn1Tag(UniversalTagNumber.OctetString, isConstructed: true), + new Asn1Tag(TagClass.Private, 87), + new Asn1Tag(TagClass.ContextSpecific, 13, isConstructed: true), + }; + + byte[] answerBuf = new byte[payloadLength + 100]; + + foreach (Asn1Tag toTry in tagsToTry) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + writer.WriteOctetString(toTry, data); + + Assert.True(writer.TryEncode(answerBuf, out _)); + Assert.True(Asn1Tag.TryParse(answerBuf, out Asn1Tag writtenTag, out _)); + + if (expectConstructed) + { + Assert.True(writtenTag.IsConstructed, $"writtenTag.IsConstructed ({toTry})"); + } + else + { + Assert.False(writtenTag.IsConstructed, $"writtenTag.IsConstructed ({toTry})"); + } + + Assert.Equal(toTry.TagClass, writtenTag.TagClass); + Assert.Equal(toTry.TagValue, writtenTag.TagValue); + } + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyWriteOctetString_EndOfContents(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteOctetString(Asn1Tag.EndOfContents, ReadOnlySpan<byte>.Empty)); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteOctetString(Asn1Tag.EndOfContents, new byte[1])); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteUtcTime.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteUtcTime.cs new file mode 100644 index 0000000000..1d52882447 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteUtcTime.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Security.Cryptography.Asn1; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteUtcTime : Asn1WriterTests + { + public static IEnumerable<object[]> TestCases { get; } = new object[][] + { + new object[] + { + new DateTimeOffset(2017, 10, 16, 8, 24, 3, TimeSpan.FromHours(-7)), + "0D3137313031363135323430335A", + }, + new object[] + { + new DateTimeOffset(1817, 10, 16, 21, 24, 3, TimeSpan.FromHours(6)), + "0D3137313031363135323430335A", + }, + new object[] + { + new DateTimeOffset(3000, 1, 1, 0, 0, 0, TimeSpan.Zero), + "0D3030303130313030303030305A", + }, + }; + + [Theory] + [MemberData(nameof(TestCases))] + public void VerifyWriteUtcTime_BER(DateTimeOffset input, string expectedHexPayload) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + writer.WriteUtcTime(input); + + Verify(writer, "17" + expectedHexPayload); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void VerifyWriteUtcTime_BER_CustomTag(DateTimeOffset input, string expectedHexPayload) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + Asn1Tag tag = new Asn1Tag(TagClass.Application, 11); + writer.WriteUtcTime(tag, input); + + Verify(writer, Stringify(tag) + expectedHexPayload); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void VerifyWriteUtcTime_CER(DateTimeOffset input, string expectedHexPayload) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + writer.WriteUtcTime(input); + + Verify(writer, "17" + expectedHexPayload); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void VerifyWriteUtcTime_CER_CustomTag(DateTimeOffset input, string expectedHexPayload) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.CER); + Asn1Tag tag = new Asn1Tag(TagClass.Private, 95); + writer.WriteUtcTime(tag, input); + + Verify(writer, Stringify(tag) + expectedHexPayload); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void VerifyWriteUtcTime_DER(DateTimeOffset input, string expectedHexPayload) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + writer.WriteUtcTime(input); + + Verify(writer, "17" + expectedHexPayload); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void VerifyWriteUtcTime_DER_CustomTag(DateTimeOffset input, string expectedHexPayload) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 3); + writer.WriteUtcTime(tag, input); + + Verify(writer, Stringify(tag) + expectedHexPayload); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyWriteUtcTime_EndOfContents(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + + AssertExtensions.Throws<ArgumentException>( + "tag", + () => writer.WriteUtcTime(Asn1Tag.EndOfContents, DateTimeOffset.Now)); + } + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public void VerifyWriteUtcTime_IgnoresConstructed(PublicEncodingRules ruleSet) + { + AsnWriter writer = new AsnWriter((AsnEncodingRules)ruleSet); + DateTimeOffset value = new DateTimeOffset(2017, 11, 16, 17, 35, 1, TimeSpan.Zero); + + writer.WriteUtcTime(new Asn1Tag(UniversalTagNumber.UtcTime, true), value); + writer.WriteUtcTime(new Asn1Tag(TagClass.ContextSpecific, 3, true), value); + Verify(writer, "170D3137313131363137333530315A" + "830D3137313131363137333530315A"); + } + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteUtf8String.cs b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteUtf8String.cs new file mode 100644 index 0000000000..c8dce7a0d0 --- /dev/null +++ b/src/System.Security.Cryptography.Encoding/tests/Asn1/Writer/WriteUtf8String.cs @@ -0,0 +1,287 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Security.Cryptography.Asn1; +using Xunit; + +namespace System.Security.Cryptography.Tests.Asn1 +{ + public class WriteUtf8String : WriteCharacterString + { + public static IEnumerable<object[]> ShortValidCases { get; } = new object[][] + { + new object[] + { + string.Empty, + "00", + }, + new object[] + { + "hi", + "026869", + }, + new object[] + { + "Dr. & Mrs. Smith\u2010Jones \uFE60 children", + "2544722E2026204D72732E20536D697468E280904A6F6E657320EFB9A0206368696C6472656E", + }, + }; + + public static IEnumerable<object[]> LongValidCases { get; } = new object[][] + { + new object[] + { + new string('f', 957) + new string('w', 182), + "820473" + new string('6', 957 * 2) + new string('7', 182 * 2), + }, + }; + + public static IEnumerable<object[]> CERSegmentedCases { get; } = new object[][] + { + new object[] + { + GettysburgAddress, + 1458, + }, + new object[] + { + // A whole bunch of "small ampersand" values (3 bytes each UTF-8), + // then one inverted exclamation (2 bytes UTF-8) + new string('\uFE60', 2000 / 3) + '\u00A1', + 2000, + }, + }; + + public static IEnumerable<object[]> InvalidInputs => Array.Empty<object[]>(); + + internal override void WriteString(AsnWriter writer, string s) => + writer.WriteCharacterString(UniversalTagNumber.UTF8String, s); + + internal override void WriteString(AsnWriter writer, Asn1Tag tag, string s) => + writer.WriteCharacterString(tag, UniversalTagNumber.UTF8String, s); + + internal override void WriteSpan(AsnWriter writer, ReadOnlySpan<char> s) => + writer.WriteCharacterString(UniversalTagNumber.UTF8String, s); + + internal override void WriteSpan(AsnWriter writer, Asn1Tag tag, ReadOnlySpan<char> s) => + writer.WriteCharacterString(tag, UniversalTagNumber.UTF8String, s); + + internal override Asn1Tag StandardTag => new Asn1Tag(UniversalTagNumber.UTF8String); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_String(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_String(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_String_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_String_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_String(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_String(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_String_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_String_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_String(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_String(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_String_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_String_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_Span(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_Span(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_Span_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_Span_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_Span(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_Span(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_Span_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_Span_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_Span(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_Span(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_Span_CustomTag(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_Span_CustomTag(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_String_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_String_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_String_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_String_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_Span_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_Span_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_BER_Span_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_BER_Span_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_String_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_String_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_String_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_String_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_Span_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_Span_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + public new void VerifyWrite_CER_Span_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_CER_Span_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_String_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_String_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_String_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_String_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_Span_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_Span_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [MemberData(nameof(ShortValidCases))] + [MemberData(nameof(LongValidCases))] + public new void VerifyWrite_DER_Span_CustomTag_ClearsConstructed(string input, string expectedPayloadHex) => + base.VerifyWrite_DER_Span_CustomTag_ClearsConstructed(input, expectedPayloadHex); + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public new void VerifyWrite_String_Null(PublicEncodingRules ruleSet) => + base.VerifyWrite_String_Null(ruleSet); + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public new void VerifyWrite_String_Null_CustomTag(PublicEncodingRules ruleSet) => + base.VerifyWrite_String_Null_CustomTag(ruleSet); + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public new void VerifyWrite_EndOfContents_String(PublicEncodingRules ruleSet) => + base.VerifyWrite_EndOfContents_String(ruleSet); + + [Theory] + [InlineData(PublicEncodingRules.BER)] + [InlineData(PublicEncodingRules.CER)] + [InlineData(PublicEncodingRules.DER)] + public new void VerifyWrite_EndOfContents_Span(PublicEncodingRules ruleSet) => + base.VerifyWrite_EndOfContents_Span(ruleSet); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_String(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_String(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_String_CustomTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_String_CustomTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_String_ConstructedTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_String_ConstructedTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_String_CustomPrimitiveTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_String_CustomPrimitiveTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_Span(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_Span(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_Span_CustomTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_Span_CustomTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_Span_ConstructedTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_Span_ConstructedTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(CERSegmentedCases))] + public new void VerifyWrite_CERSegmented_Span_CustomPrimitiveTag(string input, int contentByteCount) => + base.VerifyWrite_CERSegmented_Span_CustomPrimitiveTag(input, contentByteCount); + + [Theory] + [MemberData(nameof(InvalidInputs))] + public new void VerifyWrite_String_NonEncodable(string input) => + base.VerifyWrite_String_NonEncodable(input); + + [Theory] + [MemberData(nameof(InvalidInputs))] + public new void VerifyWrite_Span_NonEncodable(string input) => + base.VerifyWrite_Span_NonEncodable(input); + } +} diff --git a/src/System.Security.Cryptography.Encoding/tests/Resources/Strings.resx b/src/System.Security.Cryptography.Encoding/tests/Resources/Strings.resx index 59c5bc4f97..797443823e 100644 --- a/src/System.Security.Cryptography.Encoding/tests/Resources/Strings.resx +++ b/src/System.Security.Cryptography.Encoding/tests/Resources/Strings.resx @@ -61,10 +61,34 @@ <data name="Argument_InvalidOidValue" xml:space="preserve"> <value>The OID value was invalid.</value> </data> + <data name="Cryptography_Asn_EnumeratedValueRequiresNonFlagsEnum" xml:space="preserve"> + <value>ASN.1 Enumerated values only apply to enum types without the [Flags] attribute.</value> + </data> + <data name="Cryptography_Asn_NamedBitListRequiresFlagsEnum" xml:space="preserve"> + <value>Named bit list operations require an enum with the [Flags] attribute.</value> + </data> + <data name="Cryptography_Asn_NamedBitListValueTooBig" xml:space="preserve"> + <value>The encoded named bit list value is larger than the value size of the '{0}' enum.</value> + </data> + <data name="Cryptography_Asn_UniversalValueIsFixed" xml:space="preserve"> + <value>Tags with TagClass Universal must have the appropriate TagValue value for the data type being read or written.</value> + </data> + <data name="Cryptography_Asn_UnusedBitCountRange" xml:space="preserve"> + <value>Unused bit count must be between 0 and 7, inclusive.</value> + </data> + <data name="Cryptography_AsnWriter_EncodeUnbalancedStack" xml:space="preserve"> + <value>Encode cannot be called while a Sequence or SetOf is still open.</value> + </data> + <data name="Cryptography_AsnWriter_PopWrongTag" xml:space="preserve"> + <value>Cannot pop the requested tag as it is not currently in progress.</value> + </data> <data name="Cryptography_Der_Invalid_Encoding" xml:space="preserve"> <value>ASN1 corrupted data.</value> </data> <data name="Cryptography_Invalid_IA5String" xml:space="preserve"> <value>The string contains a character not in the 7 bit ASCII character set.</value> </data> + <data name="Cryptography_WriteEncodedValue_OneValueAtATime" xml:space="preserve"> + <value>The input to WriteEncodedValue must represent a single encoded value with no trailing data.</value> + </data> </root> diff --git a/src/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj b/src/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj index 8026270d7c..e32a65c910 100644 --- a/src/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj +++ b/src/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj @@ -10,6 +10,45 @@ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-Windows_NT-Debug|AnyCPU'" /> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-Windows_NT-Release|AnyCPU'" /> <ItemGroup> + <Compile Include="Asn1\Reader\Asn1ReaderTests.cs" /> + <Compile Include="Asn1\Reader\ComprehensiveReadTests.cs" /> + <Compile Include="Asn1\Reader\ParseTag.cs" /> + <Compile Include="Asn1\Reader\PeekTests.cs" /> + <Compile Include="Asn1\Reader\ReadBitString.cs" /> + <Compile Include="Asn1\Reader\ReadBMPString.cs" /> + <Compile Include="Asn1\Reader\ReadBoolean.cs" /> + <Compile Include="Asn1\Reader\ReadEnumerated.cs" /> + <Compile Include="Asn1\Reader\ReadGeneralizedTime.cs" /> + <Compile Include="Asn1\Reader\ReadIA5String.cs" /> + <Compile Include="Asn1\Reader\ReadInteger.cs" /> + <Compile Include="Asn1\Reader\ReadLength.cs" /> + <Compile Include="Asn1\Reader\ReadNull.cs" /> + <Compile Include="Asn1\Reader\ReadNamedBitList.cs" /> + <Compile Include="Asn1\Reader\ReadObjectIdentifier.cs" /> + <Compile Include="Asn1\Reader\ReadOctetString.cs" /> + <Compile Include="Asn1\Reader\ReadSequence.cs" /> + <Compile Include="Asn1\Reader\ReadSetOf.cs" /> + <Compile Include="Asn1\Reader\ReadUtcTime.cs" /> + <Compile Include="Asn1\Reader\ReadUTF8String.cs" /> + <Compile Include="Asn1\Reader\ReaderStateTests.cs" /> + <Compile Include="Asn1\Writer\Asn1WriterTests.cs" /> + <Compile Include="Asn1\Writer\ComprehensiveWriteTest.cs" /> + <Compile Include="Asn1\Writer\PushPopSequence.cs" /> + <Compile Include="Asn1\Writer\PushPopSetOf.cs" /> + <Compile Include="Asn1\Writer\WriteBitString.cs" /> + <Compile Include="Asn1\Writer\WriteBMPString.cs" /> + <Compile Include="Asn1\Writer\WriteBoolean.cs" /> + <Compile Include="Asn1\Writer\WriteCharacterString.cs" /> + <Compile Include="Asn1\Writer\WriteEnumerated.cs" /> + <Compile Include="Asn1\Writer\WriteGeneralizedTime.cs" /> + <Compile Include="Asn1\Writer\WriteIA5String.cs" /> + <Compile Include="Asn1\Writer\WriteInteger.cs" /> + <Compile Include="Asn1\Writer\WriteNamedBitList.cs" /> + <Compile Include="Asn1\Writer\WriteNull.cs" /> + <Compile Include="Asn1\Writer\WriteOctetString.cs" /> + <Compile Include="Asn1\Writer\WriteObjectIdentifier.cs" /> + <Compile Include="Asn1\Writer\WriteUtcTime.cs" /> + <Compile Include="Asn1\Writer\WriteUtf8String.cs" /> <Compile Include="AsnEncodedData.cs" /> <Compile Include="AsnEncodedDataCollectionTests.cs" /> <Compile Include="Base64TransformsTests.cs" /> @@ -17,6 +56,15 @@ <Compile Include="DerSequenceReaderTests.cs" /> <Compile Include="Oid.cs" /> <Compile Include="OidCollectionTests.cs" /> + <Compile Include="$(CommonPath)\System\Security\Cryptography\Asn1V2.cs"> + <Link>Common\System\Security\Cryptography\Asn1V2.cs</Link> + </Compile> + <Compile Include="$(CommonPath)\System\Security\Cryptography\AsnReader.cs"> + <Link>Common\System\Security\Cryptography\AsnReader.cs</Link> + </Compile> + <Compile Include="$(CommonPath)\System\Security\Cryptography\AsnWriter.cs"> + <Link>Common\System\Security\Cryptography\AsnWriter.cs</Link> + </Compile> <Compile Include="$(CommonPath)\System\Security\Cryptography\DerEncoder.cs"> <Link>Common\System\Security\Cryptography\DerEncoder.cs</Link> </Compile> |