diff --git a/src/libraries/Common/tests/CoreFx.Private.TestUtilities/System/AssertExtensions.cs b/src/libraries/Common/tests/CoreFx.Private.TestUtilities/System/AssertExtensions.cs index 99f1093e1833e..d1688c9c860b0 100644 --- a/src/libraries/Common/tests/CoreFx.Private.TestUtilities/System/AssertExtensions.cs +++ b/src/libraries/Common/tests/CoreFx.Private.TestUtilities/System/AssertExtensions.cs @@ -390,7 +390,7 @@ public static void AtLeastOneEquals(T expected1, T expected2, T value) public delegate void AssertThrowsAction(Span span); // Cannot use standard Assert.Throws() when testing Span - Span and closures don't get along. - public static void AssertThrows(ReadOnlySpan span, AssertThrowsActionReadOnly action) where E : Exception + public static E AssertThrows(ReadOnlySpan span, AssertThrowsActionReadOnly action) where E : Exception { Exception exception; @@ -404,18 +404,18 @@ public static void AtLeastOneEquals(T expected1, T expected2, T value) exception = ex; } - if (exception == null) + switch(exception) { - throw new ThrowsException(typeof(E)); - } - - if (exception.GetType() != typeof(E)) - { - throw new ThrowsException(typeof(E), exception); + case null: + throw new ThrowsException(typeof(E)); + case E ex when (ex.GetType() == typeof(E)): + return ex; + default: + throw new ThrowsException(typeof(E), exception); } } - public static void AssertThrows(Span span, AssertThrowsAction action) where E : Exception + public static E AssertThrows(Span span, AssertThrowsAction action) where E : Exception { Exception exception; @@ -429,15 +429,31 @@ public static void AtLeastOneEquals(T expected1, T expected2, T value) exception = ex; } - if (exception == null) + switch(exception) { - throw new ThrowsException(typeof(E)); + case null: + throw new ThrowsException(typeof(E)); + case E ex when (ex.GetType() == typeof(E)): + return ex; + default: + throw new ThrowsException(typeof(E), exception); } + } - if (exception.GetType() != typeof(E)) - { - throw new ThrowsException(typeof(E), exception); - } + public static E Throws(string expectedParamName, ReadOnlySpan span, AssertThrowsActionReadOnly action) + where E : ArgumentException + { + E exception = AssertThrows(span, action); + Assert.Equal(expectedParamName, exception.ParamName); + return exception; + } + + public static E Throws(string expectedParamName, Span span, AssertThrowsAction action) + where E : ArgumentException + { + E exception = AssertThrows(span, action); + Assert.Equal(expectedParamName, exception.ParamName); + return exception; } } } diff --git a/src/libraries/System.Security.Cryptography.Encoding/ref/System.Security.Cryptography.Encoding.cs b/src/libraries/System.Security.Cryptography.Encoding/ref/System.Security.Cryptography.Encoding.cs index b42ed80dcd8b5..b471f98baa3d1 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/ref/System.Security.Cryptography.Encoding.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/ref/System.Security.Cryptography.Encoding.cs @@ -109,6 +109,23 @@ public enum OidGroup Template = 9, KeyDerivationFunction = 10, } + public static partial class PemEncoding + { + public static System.Security.Cryptography.PemFields Find(System.ReadOnlySpan pemData) { throw null; } + public static int GetEncodedSize(int labelLength, int dataLength) { throw null; } + public static bool TryFind(System.ReadOnlySpan pemData, out System.Security.Cryptography.PemFields fields) { throw null; } + public static bool TryWrite(System.ReadOnlySpan label, System.ReadOnlySpan data, System.Span destination, out int charsWritten) { throw null; } + public static char[] Write(System.ReadOnlySpan label, System.ReadOnlySpan data) { throw null; } + } + public readonly partial struct PemFields + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public System.Range Base64Data { get { throw null; } } + public int DecodedDataLength { get { throw null; } } + public System.Range Label { get { throw null; } } + public System.Range Location { get { throw null; } } + } public partial class ToBase64Transform : System.IDisposable, System.Security.Cryptography.ICryptoTransform { public ToBase64Transform() { } diff --git a/src/libraries/System.Security.Cryptography.Encoding/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography.Encoding/src/Resources/Strings.resx index 54f8f7c1f73b0..3b48346514c4c 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography.Encoding/src/Resources/Strings.resx @@ -117,6 +117,18 @@ Value was invalid. + + No PEM encoded data found. + + + The specified label is not valid. + + + The encoded PEM size is too large to represent as a signed 32-bit integer. + + + A positive number is required. + Cannot access a disposed object. diff --git a/src/libraries/System.Security.Cryptography.Encoding/src/System.Security.Cryptography.Encoding.csproj b/src/libraries/System.Security.Cryptography.Encoding/src/System.Security.Cryptography.Encoding.csproj index 0fa3db009eb3a..233e12628355f 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/src/System.Security.Cryptography.Encoding.csproj +++ b/src/libraries/System.Security.Cryptography.Encoding/src/System.Security.Cryptography.Encoding.csproj @@ -18,6 +18,8 @@ + + Internal\Cryptography\Helpers.cs @@ -147,4 +149,4 @@ - \ No newline at end of file + diff --git a/src/libraries/System.Security.Cryptography.Encoding/src/System/Security/Cryptography/PemEncoding.cs b/src/libraries/System.Security.Cryptography.Encoding/src/System/Security/Cryptography/PemEncoding.cs new file mode 100644 index 0000000000000..7232a72205c9c --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/src/System/Security/Cryptography/PemEncoding.cs @@ -0,0 +1,538 @@ +// 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.Runtime.CompilerServices; + +namespace System.Security.Cryptography +{ + /// + /// Provides methods for reading and writing the IETF RFC 7468 + /// subset of PEM (Privacy-Enhanced Mail) textual encodings. + /// This class cannot be inherited. + /// + public static class PemEncoding + { + private const string PreEBPrefix = "-----BEGIN "; + private const string PostEBPrefix = "-----END "; + private const string Ending = "-----"; + private const int EncodedLineLength = 64; + + /// + /// Finds the first PEM-encoded data. + /// + /// + /// The text containing the PEM-encoded data. + /// + /// + /// does not contain a well-formed PEM-encoded value. + /// + /// + /// A value that specifies the location, label, and data location of + /// the encoded data. + /// + /// + /// IETF RFC 7468 permits different decoding rules. This method + /// always uses lax rules. + /// + public static PemFields Find(ReadOnlySpan pemData) + { + if (!TryFind(pemData, out PemFields fields)) + { + throw new ArgumentException(SR.Argument_PemEncoding_NoPemFound, nameof(pemData)); + } + + return fields; + } + + /// + /// Attempts to find the first PEM-encoded data. + /// + /// + /// The text containing the PEM-encoded data. + /// + /// + /// When this method returns, contains a value + /// that specifies the location, label, and data location of the encoded data; + /// or that specifies those locations as empty if no PEM-encoded data is found. + /// This parameter is treated as uninitialized. + /// + /// + /// true if PEM-encoded data was found; otherwise false. + /// + /// + /// IETF RFC 7468 permits different decoding rules. This method + /// always uses lax rules. + /// + public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) + { + // Check for the minimum possible encoded length of a PEM structure + // and exit early if there is no way the input could contain a well-formed + // PEM. + if (pemData.Length < PreEBPrefix.Length + Ending.Length * 2 + PostEBPrefix.Length) + { + fields = default; + return false; + } + + const int PostebStackBufferSize = 256; + Span postebStackBuffer = stackalloc char[PostebStackBufferSize]; + int areaOffset = 0; + int preebIndex; + while ((preebIndex = pemData.IndexOfByOffset(PreEBPrefix, areaOffset)) >= 0) + { + int labelStartIndex = preebIndex + PreEBPrefix.Length; + + // If there are any previous characters, the one prior to the PreEB + // must be a white space character. + if (preebIndex > 0 && !IsWhiteSpaceCharacter(pemData[preebIndex - 1])) + { + areaOffset += labelStartIndex; + continue; + } + + int preebEndIndex = pemData.IndexOfByOffset(Ending, labelStartIndex); + + // There is no ending sequence, -----, in the remainder of + // the document. Therefore, there can never be a complete PreEB + // and we can exit. + if (preebEndIndex < 0) + { + fields = default; + return false; + } + + Range labelRange = labelStartIndex..preebEndIndex; + ReadOnlySpan label = pemData[labelRange]; + + // There could be a preeb that is valid after this one if it has an invalid + // label, so move from there. + if (!IsValidLabel(label)) + { + goto NextAfterLabel; + } + + int contentStartIndex = preebEndIndex + Ending.Length; + int postebLength = PostEBPrefix.Length + label.Length + Ending.Length; + + Span postebBuffer = postebLength > PostebStackBufferSize + ? new char[postebLength] + : postebStackBuffer; + ReadOnlySpan posteb = WritePostEB(label, postebBuffer); + int postebStartIndex = pemData.IndexOfByOffset(posteb, contentStartIndex); + + if (postebStartIndex < 0) + { + goto NextAfterLabel; + } + + int pemEndIndex = postebStartIndex + postebLength; + + // The PostEB must either end at the end of the string, or + // have at least one white space character after it. + if (pemEndIndex < pemData.Length - 1 && + !IsWhiteSpaceCharacter(pemData[pemEndIndex])) + { + goto NextAfterLabel; + } + + Range contentRange = contentStartIndex..postebStartIndex; + + if (!TryCountBase64(pemData[contentRange], out int base64start, out int base64end, out int decodedSize)) + { + goto NextAfterLabel; + } + + Range pemRange = preebIndex..pemEndIndex; + Range base64range = (contentStartIndex + base64start)..(contentStartIndex + base64end); + fields = new PemFields(labelRange, base64range, pemRange, decodedSize); + return true; + + NextAfterLabel: + if (preebEndIndex <= 0) + { + // We somehow ended up in a situation where we will advance + // 0 or -1 characters, which means we'll probably end up here again, + // advancing 0 or -1 characters, in a loop. To avoid getting stuck, + // detect this situation and return. + fields = default; + return false; + } + areaOffset += preebEndIndex; + } + + fields = default; + return false; + + static ReadOnlySpan WritePostEB(ReadOnlySpan label, Span destination) + { + int size = PostEBPrefix.Length + label.Length + Ending.Length; + Debug.Assert(destination.Length >= size); + PostEBPrefix.AsSpan().CopyTo(destination); + label.CopyTo(destination.Slice(PostEBPrefix.Length)); + Ending.AsSpan().CopyTo(destination.Slice(PostEBPrefix.Length + label.Length)); + return destination.Slice(0, size); + } + } + + private static int IndexOfByOffset(this ReadOnlySpan str, ReadOnlySpan value, int startPosition) + { + int index = str.Slice(startPosition).IndexOf(value); + return index == -1 ? -1 : index + startPosition; + } + + private static bool IsValidLabel(ReadOnlySpan data) + { + static bool IsLabelChar(char c) => (uint)(c - 0x21u) <= 0x5du && c != '-'; + + // Empty labels are permitted per RFC 7468. + if (data.IsEmpty) + return true; + + // The first character must be a labelchar, so initialize to false + bool previousIsLabelChar = false; + + for (int index = 0; index < data.Length; index++) + { + char c = data[index]; + + if (IsLabelChar(c)) + { + previousIsLabelChar = true; + continue; + } + + bool isSpaceOrHyphen = c == ' ' || c == '-'; + + // IETF RFC 7468 states that every character in a label must + // be a labelchar, and each labelchar may have zero or one + // preceding space or hyphen, except the first labelchar. + // If this character is not a space or hyphen, then this characer + // is invalid. + // If it is a space or hyphen, and the previous character was + // also not a labelchar (another hyphen or space), then we have + // two consecutive spaces or hyphens which is is invalid. + if (!isSpaceOrHyphen || !previousIsLabelChar) + { + return false; + } + + previousIsLabelChar = false; + } + + // The last character must also be a labelchar. It cannot be a + // hyphen or space since these are only allowed to precede + // a labelchar. + return previousIsLabelChar; + } + + private static bool TryCountBase64( + ReadOnlySpan str, + out int base64Start, + out int base64End, + out int base64DecodedSize) + { + base64Start = 0; + base64End = str.Length; + + if (str.IsEmpty) + { + base64DecodedSize = 0; + return true; + } + + int significantCharacters = 0; + int paddingCharacters = 0; + + for (int i = 0; i < str.Length; i++) + { + char ch = str[i]; + + if (IsWhiteSpaceCharacter(ch)) + { + if (significantCharacters == 0) + { + base64Start++; + } + else + { + base64End--; + } + + continue; + } + + base64End = str.Length; + + if (ch == '=') + { + paddingCharacters++; + } + else if (paddingCharacters == 0 && IsBase64Character(ch)) + { + significantCharacters++; + } + else + { + base64DecodedSize = 0; + return false; + } + } + + int totalChars = paddingCharacters + significantCharacters; + + if (paddingCharacters > 2 || (totalChars & 0b11) != 0) + { + base64DecodedSize = 0; + return false; + } + + base64DecodedSize = (totalChars >> 2) * 3 - paddingCharacters; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsBase64Character(char ch) + { + uint c = (uint)ch; + return c == '+' || c == '/' || + c - '0' < 10 || c - 'A' < 26 || c - 'a' < 26; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsWhiteSpaceCharacter(char ch) + { + // Match white space characters from Convert.Base64 + return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; + } + + /// + /// Determines the length of a PEM-encoded value, in characters, + /// given the length of a label and binary data. + /// + /// + /// The length of the label, in characters. + /// + /// + /// The length of the data, in bytes. + /// + /// + /// The number of characters in the encoded PEM. + /// + /// + /// is a negative value. + /// + /// -or- + /// + /// is a negative value. + /// + /// -or- + /// + /// exceeds the maximum possible label length. + /// + /// -or- + /// + /// exceeds the maximum possible encoded data length. + /// + /// + /// The length of the PEM-encoded value is larger than . + /// + public static int GetEncodedSize(int labelLength, int dataLength) + { + // The largest possible label is MaxLabelSize - when included in the posteb + // and preeb lines new lines, assuming the base64 content is empty. + // -----BEGIN {char * MaxLabelSize}-----\n + // -----END {char * MaxLabelSize}----- + const int MaxLabelSize = 1_073_741_808; + + // The largest possible binary value to fit in a padded base64 string + // is 1,610,612,733 bytes. RFC 7468 states: + // Generators MUST wrap the base64-encoded lines so that each line + // consists of exactly 64 characters except for the final line + // We need to account for new line characters, every 64 characters. + // This works out to 1,585,834,053 maximum bytes in data when wrapping + // is accounted for assuming an empty label. + const int MaxDataLength = 1_585_834_053; + + if (labelLength < 0) + throw new ArgumentOutOfRangeException(nameof(labelLength), SR.ArgumentOutOfRange_NeedPositiveNumber); + if (dataLength < 0) + throw new ArgumentOutOfRangeException(nameof(dataLength), SR.ArgumentOutOfRange_NeedPositiveNumber); + if (labelLength > MaxLabelSize) + throw new ArgumentOutOfRangeException(nameof(labelLength), SR.Argument_PemEncoding_EncodedSizeTooLarge); + if (dataLength > MaxDataLength) + throw new ArgumentOutOfRangeException(nameof(dataLength), SR.Argument_PemEncoding_EncodedSizeTooLarge); + + int preebLength = PreEBPrefix.Length + labelLength + Ending.Length; + int postebLength = PostEBPrefix.Length + labelLength + Ending.Length; + int totalEncapLength = preebLength + postebLength + 1; //Add one for newline after preeb + + // dataLength is already known to not overflow here + int encodedDataLength = ((dataLength + 2) / 3) << 2; + int lineCount = Math.DivRem(encodedDataLength, EncodedLineLength, out int remainder); + + if (remainder > 0) + lineCount++; + + int encodedDataLengthWithBreaks = encodedDataLength + lineCount; + + if (int.MaxValue - encodedDataLengthWithBreaks < totalEncapLength) + throw new ArgumentException(SR.Argument_PemEncoding_EncodedSizeTooLarge); + + return encodedDataLengthWithBreaks + totalEncapLength; + } + + /// + /// Tries to write the provided data and label as PEM-encoded data into + /// a provided buffer. + /// + /// + /// The label to write. + /// + /// + /// The data to write. + /// + /// + /// The buffer to receive the PEM-encoded text. + /// + /// + /// When this method returns, this parameter contains the number of characters + /// written to . This parameter is treated + /// as uninitialized. + /// + /// + /// true if is large enough to contain + /// the PEM-encoded text, otherwise false. + /// + /// + /// This method always wraps the base-64 encoded text to 64 characters, per the + /// recommended wrapping of IETF RFC 7468. Unix-style line endings are used for line breaks. + /// + /// + /// exceeds the maximum possible label length. + /// + /// -or- + /// + /// exceeds the maximum possible encoded data length. + /// + /// + /// The resulting PEM-encoded text is larger than . + /// + /// - or - + /// + /// contains invalid characters. + /// + public static bool TryWrite(ReadOnlySpan label, ReadOnlySpan data, Span destination, out int charsWritten) + { + static int Write(ReadOnlySpan str, Span dest, int offset) + { + str.CopyTo(dest.Slice(offset)); + return str.Length; + } + + static int WriteBase64(ReadOnlySpan bytes, Span dest, int offset) + { + bool success = Convert.TryToBase64Chars(bytes, dest.Slice(offset), out int base64Written); + + if (!success) + { + Debug.Fail("Convert.TryToBase64Chars failed with a pre-sized buffer"); + throw new ArgumentException(); + } + + return base64Written; + } + + if (!IsValidLabel(label)) + throw new ArgumentException(SR.Argument_PemEncoding_InvalidLabel, nameof(label)); + + const string NewLine = "\n"; + const int BytesPerLine = 48; + int encodedSize = GetEncodedSize(label.Length, data.Length); + + if (destination.Length < encodedSize) + { + charsWritten = 0; + return false; + } + + charsWritten = 0; + charsWritten += Write(PreEBPrefix, destination, charsWritten); + charsWritten += Write(label, destination, charsWritten); + charsWritten += Write(Ending, destination, charsWritten); + charsWritten += Write(NewLine, destination, charsWritten); + + ReadOnlySpan remainingData = data; + while (remainingData.Length >= BytesPerLine) + { + charsWritten += WriteBase64(remainingData.Slice(0, BytesPerLine), destination, charsWritten); + charsWritten += Write(NewLine, destination, charsWritten); + remainingData = remainingData.Slice(BytesPerLine); + } + + Debug.Assert(remainingData.Length < BytesPerLine); + + if (remainingData.Length > 0) + { + charsWritten += WriteBase64(remainingData, destination, charsWritten); + charsWritten += Write(NewLine, destination, charsWritten); + remainingData = default; + } + + charsWritten += Write(PostEBPrefix, destination, charsWritten); + charsWritten += Write(label, destination, charsWritten); + charsWritten += Write(Ending, destination, charsWritten); + + return true; + } + + /// + /// Creates an encoded PEM with the given label and data. + /// + /// + /// The label to encode. + /// + /// + /// The data to encode. + /// + /// + /// A character array of the encoded PEM. + /// + /// + /// This method always wraps the base-64 encoded text to 64 characters, per the + /// recommended wrapping of RFC-7468. Unix-style line endings are used for line breaks. + /// + /// + /// exceeds the maximum possible label length. + /// + /// -or- + /// + /// exceeds the maximum possible encoded data length. + /// + /// + /// The resulting PEM-encoded text is larger than . + /// + /// - or - + /// + /// contains invalid characters. + /// + public static char[] Write(ReadOnlySpan label, ReadOnlySpan data) + { + if (!IsValidLabel(label)) + throw new ArgumentException(SR.Argument_PemEncoding_InvalidLabel, nameof(label)); + + int encodedSize = GetEncodedSize(label.Length, data.Length); + char[] buffer = new char[encodedSize]; + + if (!TryWrite(label, data, buffer, out int charsWritten)) + { + Debug.Fail("TryWrite failed with a pre-sized buffer"); + throw new ArgumentException(); + } + + Debug.Assert(charsWritten == encodedSize); + return buffer; + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Encoding/src/System/Security/Cryptography/PemFields.cs b/src/libraries/System.Security.Cryptography.Encoding/src/System/Security/Cryptography/PemFields.cs new file mode 100644 index 0000000000000..e4e75a03237cd --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/src/System/Security/Cryptography/PemFields.cs @@ -0,0 +1,40 @@ +// 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. + +namespace System.Security.Cryptography +{ + /// + /// Contains information about the location of PEM data. + /// + public readonly struct PemFields + { + internal PemFields(Range label, Range base64data, Range location, int decodedDataLength) + { + Location = location; + DecodedDataLength = decodedDataLength; + Base64Data = base64data; + Label = label; + } + + /// + /// Gets the location of the PEM-encoded text, including the surrounding encapsulation boundaries. + /// + public Range Location { get; } + + /// + /// Gets the location of the label. + /// + public Range Label { get; } + + /// + /// Gets the location of the base-64 data inside of the PEM. + /// + public Range Base64Data { get; } + + /// + /// Gets the size of the decoded base-64 data, in bytes. + /// + public int DecodedDataLength { get; } + } +} diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs new file mode 100644 index 0000000000000..874b862cd4cf8 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs @@ -0,0 +1,439 @@ +// 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; +using Xunit; + +namespace System.Security.Cryptography.Encoding.Tests +{ + public abstract class PemEncodingFindTests + { + [Fact] + public void Find_Success_Simple() + { + string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + PemFields fields = AssertPemFound(content, + expectedLocation: 0..44, + expectedBase64: 21..25, + expectedLabel: 11..15); + Assert.Equal("TEST", content[fields.Label]); + Assert.Equal(content, content[fields.Location]); + Assert.Equal("Zm9v", content[fields.Base64Data]); + Assert.Equal(3, fields.DecodedDataLength); + } + + [Fact] + public void Find_Success_IncompletePreebPrefixed() + { + string content = "-----BEGIN FAIL -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + AssertPemFound(content, + expectedLocation: 16..60, + expectedBase64: 37..41, + expectedLabel: 27..31); + } + + [Fact] + public void Find_Success_CompletePreebPrefixedDifferentLabel() + { + string content = "-----BEGIN FAIL----- -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + PemFields fields = AssertPemFound(content, + expectedLocation: 21..65, + expectedBase64: 42..46, + expectedLabel: 32..36); + + Assert.Equal("TEST", content[fields.Label]); + } + + [Fact] + public void Find_Success_CompletePreebPrefixedSameLabel() + { + string content = "-----BEGIN TEST----- -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + PemFields fields = AssertPemFound(content, + expectedLocation: 21..65, + expectedBase64: 42..46, + expectedLabel: 32..36); + + Assert.Equal("TEST", content[fields.Label]); + } + + [Fact] + public void Find_Success_PreebEndingOverlap() + { + string content = "-----BEGIN TEST -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + PemFields fields = AssertPemFound(content, + expectedLocation: 16..60, + expectedBase64: 37..41, + expectedLabel: 27..31); + + Assert.Equal("TEST", content[fields.Label]); + Assert.Equal(3, fields.DecodedDataLength); + } + + [Fact] + public void Find_Success_LargeLabel() + { + string label = new string('A', 275); + string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; + PemFields fields = AssertPemFound(content, + expectedLocation: 0..586, + expectedBase64: 292..296, + expectedLabel: 11..286); + + Assert.Equal(label, content[fields.Label]); + } + + [Fact] + public void Find_Success_Minimum() + { + string content = "-----BEGIN ----------END -----"; + PemFields fields = AssertPemFound(content, + expectedLocation: 0..30, + expectedBase64: 16..16, + expectedLabel: 11..11); + Assert.Equal(0, fields.DecodedDataLength); + } + + [Fact] + public void Find_Success_PrecedingContentAndWhitespaceBeforePreeb() + { + string content = "boop -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + AssertPemFound(content, + expectedLocation: 7..51, + expectedBase64: 28..32, + expectedLabel: 18..22); + } + + [Fact] + public void Find_Success_TrailingWhitespaceAfterPosteb() + { + string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST----- "; + AssertPemFound(content, + expectedLocation: 0..44, + expectedBase64: 21..25, + expectedLabel: 11..15); + } + + [Fact] + public void Find_Success_EmptyLabel() + { + string content = "-----BEGIN -----\nZm9v\n-----END -----"; + AssertPemFound(content, + expectedLocation: 0..36, + expectedBase64: 17..21, + expectedLabel: 11..11); + } + + [Fact] + public void Find_Success_EmptyContent_OneLine() + { + string content = "-----BEGIN EMPTY----------END EMPTY-----"; + PemFields fields = AssertPemFound(content, + expectedLocation: 0..40, + expectedBase64: 21..21, + expectedLabel: 11..16); + Assert.Equal(0, fields.DecodedDataLength); + } + + [Fact] + public void Find_Success_EmptyContent_ManyLinesOfWhitespace() + { + string content = "-----BEGIN EMPTY-----\n\t\n\t\n\t \n-----END EMPTY-----"; + PemFields fields = AssertPemFound(content, + expectedLocation: 0..49, + expectedBase64: 30..30, + expectedLabel: 11..16); + Assert.Equal(0, fields.DecodedDataLength); + } + + [Theory] + [InlineData("CERTIFICATE")] + [InlineData("X509 CRL")] + [InlineData("PKCS7")] + [InlineData("PRIVATE KEY")] + [InlineData("RSA PRIVATE KEY")] + public void Find_Success_CommonLabels(string label) + { + string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; + PemFields fields = FindPem(content); + Assert.Equal(label, content[fields.Label]); + } + + [Theory] + [InlineData("H E L L O")] + [InlineData("H-E-L-L-O")] + [InlineData("HEL-LO")] + public void Find_Success_LabelsWithHyphenSpace(string label) + { + string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; + PemFields fields = FindPem(content); + Assert.Equal(label, content[fields.Label]); + } + + [Fact] + public void Find_Success_SingleLetterLabel() + { + string content = "-----BEGIN H-----\nZm9v\n-----END H-----"; + AssertPemFound(content, + expectedLocation: 0..38, + expectedBase64: 18..22, + expectedLabel: 11..12); + } + + [Fact] + public void Find_Success_LabelCharacterBoundaries() + { + string content = $"-----BEGIN !PANIC~~~-----\nAHHH\n-----END !PANIC~~~-----"; + PemFields fields = AssertPemFound(content, + expectedLocation: 0..54, + expectedBase64: 26..30, + expectedLabel: 11..20); + } + + [Theory] + [InlineData(" ")] + [InlineData("\n")] + [InlineData("\r")] + [InlineData("\t")] + public void Find_Success_WhiteSpaceBeforePreebSeparatesFromPriorContent(string whiteSpace) + { + string content = $"blah{whiteSpace}-----BEGIN TEST-----\nZn9v\n-----END TEST-----"; + PemFields fields = AssertPemFound(content, + expectedLocation: 5..49, + expectedBase64: 26..30, + expectedLabel: 16..20); + } + + [Theory] + [InlineData(" ")] + [InlineData("\n")] + [InlineData("\r")] + [InlineData("\t")] + public void Find_Success_WhiteSpaceAfterPpostebSeparatesFromSubsequentContent(string whiteSpace) + { + string content = $"-----BEGIN TEST-----\nZn9v\n-----END TEST-----{whiteSpace}blah"; + PemFields fields = AssertPemFound(content, + expectedLocation: 0..44, + expectedBase64: 21..25, + expectedLabel: 11..15); + } + + [Fact] + public void Find_Success_Base64SurroundingWhiteSpaceStripped() + { + string content = $"-----BEGIN A-----\r\n Zm9v\n\r \t-----END A-----"; + PemFields fields = AssertPemFound(content, + expectedLocation: 0..43, + expectedBase64: 20..24, + expectedLabel: 11..12); + } + + [Fact] + public void Find_Success_FindsPemAfterPemWithInvalidBase64() + { + string content = @" +-----BEGIN TEST----- +$$$$ +-----END TEST----- +-----BEGIN TEST2----- +Zm9v +-----END TEST2-----"; + PemFields fields = FindPem(content); + Assert.Equal("TEST2", content[fields.Label]); + Assert.Equal("Zm9v", content[fields.Base64Data]); + } + + [Fact] + public void Find_Success_FindsPemAfterPemWithInvalidLabel() + { + string content = @" +-----BEGIN ------ +YmFy +-----END ------ +-----BEGIN TEST2----- +Zm9v +-----END TEST2-----"; + + PemFields fields = FindPem(content); + Assert.Equal("TEST2", content[fields.Label]); + Assert.Equal("Zm9v", content[fields.Base64Data]); + } + + [Fact] + public void Find_Fail_Empty() + { + AssertNoPemFound(string.Empty); + } + + [Fact] + public void Find_Fail_PostEbBeforePreEb() + { + string content = "-----END TEST-----\n-----BEGIN TEST-----\nZm9v"; + AssertNoPemFound(content); + } + + [Theory] + [InlineData("\tOOPS")] + [InlineData(" OOPS")] + [InlineData(" ")] + [InlineData("-")] + [InlineData("-OOPS")] + [InlineData("te\x7fst")] + [InlineData("te\x19st")] + [InlineData("te st")] //two spaces + [InlineData("te- st")] + [InlineData("test ")] //last is space, must be labelchar + [InlineData("test-")] //last is hyphen, must be labelchar + public void Find_Fail_InvalidLabel(string label) + { + string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; + AssertNoPemFound(content); + } + + [Fact] + public void Find_Fail_InvalidBase64() + { + string content = "-----BEGIN TEST-----\n$$$$\n-----END TEST-----"; + AssertNoPemFound(content); + } + + [Fact] + public void Find_Fail_PrecedingLinesAndSignificantCharsBeforePreeb() + { + string content = "boop\nbeep-----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + AssertNoPemFound(content); + } + + + [Theory] + [InlineData("\u200A")] // hair space + [InlineData("\v")] + [InlineData("\f")] + public void Find_Fail_NotPermittedWhiteSpaceSeparatorsForPreeb(string whiteSpace) + { + string content = $"boop{whiteSpace}-----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + AssertNoPemFound(content); + } + + [Theory] + [InlineData("\u200A")] // hair space + [InlineData("\v")] + [InlineData("\f")] + public void Find_Fail_NotPermittedWhiteSpaceSeparatorsForPosteb(string whiteSpace) + { + string content = $"-----BEGIN TEST-----\nZm9v\n-----END TEST-----{whiteSpace}boop"; + AssertNoPemFound(content); + } + + [Fact] + public void Find_Fail_ContentOnPostEbLine() + { + string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST-----boop"; + AssertNoPemFound(content); + } + + [Fact] + public void Find_Fail_MismatchedLabels() + { + string content = "-----BEGIN TEST-----\nZm9v\n-----END FAIL-----"; + AssertNoPemFound(content); + } + + [Fact] + public void Find_Fail_NoPostEncapBoundary() + { + string content = "-----BEGIN TEST-----\nZm9v\n"; + AssertNoPemFound(content); + } + + [Fact] + public void Find_Fail_IncompletePostEncapBoundary() + { + string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST"; + AssertNoPemFound(content); + } + + [Fact] + public void Find_Fail_InvalidBase64_Size() + { + string content = "-----BEGIN TEST-----\nZ\n-----END TEST-----"; + AssertNoPemFound(content); + } + + [Fact] + public void Find_Fail_InvalidBase64_ExtraPadding() + { + string content = "-----BEGIN TEST-----\nZm9v====\n-----END TEST-----"; + AssertNoPemFound(content); + } + + [Fact] + public void Find_Fail_InvalidBase64_MissingPadding() + { + string content = "-----BEGIN TEST-----\nZm8\n-----END TEST-----"; + AssertNoPemFound(content); + } + + [Theory] + [InlineData("", 0)] + [InlineData("cA==", 1)] + [InlineData("cGU=", 2)] + [InlineData("cGVu", 3)] + [InlineData("cGVubg==", 4)] + [InlineData("cGVubnk=", 5)] + [InlineData("cGVubnkh", 6)] + [InlineData("c G V u b n k h", 6)] + public void Find_Success_DecodeSize(string base64, int expectedSize) + { + string content = $"-----BEGIN TEST-----\n{base64}\n-----END TEST-----"; + PemFields fields = FindPem(content); + Assert.Equal(expectedSize, fields.DecodedDataLength); + Assert.Equal(base64, content[fields.Base64Data]); + } + + private PemFields AssertPemFound( + ReadOnlySpan input, + Range expectedLocation, + Range expectedBase64, + Range expectedLabel) + { + PemFields fields = FindPem(input); + Assert.Equal(expectedBase64, fields.Base64Data); + Assert.Equal(expectedLocation, fields.Location); + Assert.Equal(expectedLabel, fields.Label); + + return fields; + } + + protected abstract void AssertNoPemFound(ReadOnlySpan input); + + protected abstract PemFields FindPem(ReadOnlySpan input); + } + + public class PemEncodingFindThrowingTests : PemEncodingFindTests + { + protected override PemFields FindPem(ReadOnlySpan input) => PemEncoding.Find(input); + + protected override void AssertNoPemFound(ReadOnlySpan input) + { + AssertExtensions.Throws("pemData", input, x => PemEncoding.Find(x)); + } + } + + public class PemEncodingFindTryTests : PemEncodingFindTests + { + protected override PemFields FindPem(ReadOnlySpan input) + { + bool found = PemEncoding.TryFind(input, out PemFields fields); + Assert.True(found, "Did not find PEM."); + return fields; + } + + protected override void AssertNoPemFound(ReadOnlySpan input) + { + bool found = PemEncoding.TryFind(input, out _); + Assert.False(found, "Found PEM when not expected"); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs new file mode 100644 index 0000000000000..ec262a5e6f566 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -0,0 +1,309 @@ +// 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; +using Xunit; + +namespace System.Security.Cryptography.Encoding.Tests +{ + public static class PemEncodingTests + { + [Fact] + public static void GetEncodedSize_Empty() + { + int size = PemEncoding.GetEncodedSize(labelLength: 0, dataLength: 0); + Assert.Equal(31, size); + } + + [Theory] + [InlineData(1, 0, 33)] + [InlineData(1, 1, 38)] + [InlineData(16, 2048, 2838)] + public static void GetEncodedSize_Simple(int labelLength, int dataLength, int expectedSize) + { + int size = PemEncoding.GetEncodedSize(labelLength, dataLength); + Assert.Equal(expectedSize, size); + } + + [Theory] + [InlineData(1_073_741_808, 0, int.MaxValue)] + [InlineData(1_073_741_805, 1, int.MaxValue - 1)] + [InlineData(0, 1_585_834_053, int.MaxValue - 2)] + [InlineData(1, 1_585_834_053, int.MaxValue)] + public static void GetEncodedSize_Boundaries(int labelLength, int dataLength, int expectedSize) + { + int size = PemEncoding.GetEncodedSize(labelLength, dataLength); + Assert.Equal(expectedSize, size); + } + + [Fact] + public static void GetEncodedSize_LabelLength_Overflow() + { + AssertExtensions.Throws("labelLength", + () => PemEncoding.GetEncodedSize(labelLength: 1_073_741_809, dataLength: 0)); + } + + [Fact] + public static void GetEncodedSize_DataLength_Overflow() + { + AssertExtensions.Throws("dataLength", + () => PemEncoding.GetEncodedSize(labelLength: 0, dataLength: 1_585_834_054)); + } + + [Fact] + public static void GetEncodedSize_Combined_Overflow() + { + Assert.Throws( + () => PemEncoding.GetEncodedSize(labelLength: 2, dataLength: 1_585_834_052)); + } + + [Fact] + public static void GetEncodedSize_DataLength_Negative() + { + AssertExtensions.Throws("dataLength", + () => PemEncoding.GetEncodedSize(labelLength: 0, dataLength: -1)); + } + + [Fact] + public static void GetEncodedSize_LabelLength_Negative() + { + AssertExtensions.Throws("labelLength", + () => PemEncoding.GetEncodedSize(labelLength: -1, dataLength: 0)); + } + + [Fact] + public static void TryWrite_Simple() + { + char[] buffer = new char[1000]; + string label = "HELLO"; + byte[] content = new byte[] { 0x66, 0x6F, 0x6F }; + Assert.True(PemEncoding.TryWrite(label, content, buffer, out int charsWritten)); + string pem = new string(buffer, 0, charsWritten); + Assert.Equal("-----BEGIN HELLO-----\nZm9v\n-----END HELLO-----", pem); + } + + [Fact] + public static void Write_Simple() + { + string label = "HELLO"; + byte[] content = new byte[] { 0x66, 0x6F, 0x6F }; + char[] result = PemEncoding.Write(label, content); + string pem = new string(result); + Assert.Equal("-----BEGIN HELLO-----\nZm9v\n-----END HELLO-----", pem); + } + + [Fact] + public static void TryWrite_Empty() + { + char[] buffer = new char[31]; + Assert.True(PemEncoding.TryWrite(default, default, buffer, out int charsWritten)); + string pem = new string(buffer, 0, charsWritten); + Assert.Equal("-----BEGIN -----\n-----END -----", pem); + } + + [Fact] + public static void Write_Empty() + { + char[] result = PemEncoding.Write(default, default); + string pem = new string(result); + Assert.Equal("-----BEGIN -----\n-----END -----", pem); + } + + [Fact] + public static void TryWrite_BufferTooSmall() + { + char[] buffer = new char[30]; + Assert.False(PemEncoding.TryWrite(default, default, buffer, out _)); + } + + [Fact] + public static void TryWrite_ExactLineNoPadding() + { + char[] buffer = new char[1000]; + ReadOnlySpan data = new byte[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7 + }; + string label = "FANCY DATA"; + Assert.True(PemEncoding.TryWrite(label, data, buffer, out int charsWritten)); + string pem = new string(buffer, 0, charsWritten); + string expected = + "-----BEGIN FANCY DATA-----\n" + + "AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYH\n" + + "-----END FANCY DATA-----"; + Assert.Equal(expected, pem); + } + + [Fact] + public static void Write_ExactLineNoPadding() + { + ReadOnlySpan data = new byte[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7 + }; + string label = "FANCY DATA"; + char[] result = PemEncoding.Write(label, data); + string pem = new string(result); + string expected = + "-----BEGIN FANCY DATA-----\n" + + "AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYH\n" + + "-----END FANCY DATA-----"; + Assert.Equal(expected, pem); + } + + [Fact] + public static void TryWrite_DoesNotWriteOutsideBounds() + { + Span buffer = new char[1000]; + buffer.Fill('!'); + ReadOnlySpan data = new byte[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7 + }; + + Span write = buffer[10..]; + string label = "FANCY DATA"; + Assert.True(PemEncoding.TryWrite(label, data, write, out int charsWritten)); + string pem = new string(buffer[..(charsWritten + 20)]); + string expected = + "!!!!!!!!!!-----BEGIN FANCY DATA-----\n" + + "AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYH\n" + + "-----END FANCY DATA-----!!!!!!!!!!"; + Assert.Equal(expected, pem); + } + + [Fact] + public static void TryWrite_WrapPadding() + { + char[] buffer = new char[1000]; + ReadOnlySpan data = new byte[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + }; + string label = "UNFANCY DATA"; + Assert.True(PemEncoding.TryWrite(label, data, buffer, out int charsWritten)); + string pem = new string(buffer, 0, charsWritten); + string expected = + "-----BEGIN UNFANCY DATA-----\n" + + "AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYH\n" + + "CAk=\n" + + "-----END UNFANCY DATA-----"; + Assert.Equal(expected, pem); + } + + [Fact] + public static void Write_WrapPadding() + { + ReadOnlySpan data = new byte[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + }; + string label = "UNFANCY DATA"; + char[] result = PemEncoding.Write(label, data); + string pem = new string(result); + string expected = + "-----BEGIN UNFANCY DATA-----\n" + + "AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYH\n" + + "CAk=\n" + + "-----END UNFANCY DATA-----"; + Assert.Equal(expected, pem); + } + + [Fact] + public static void TryWrite_EcKey() + { + char[] buffer = new char[1000]; + ReadOnlySpan data = new byte[] { + 0x30, 0x74, 0x02, 0x01, 0x01, 0x04, 0x20, 0x20, + 0x59, 0xef, 0xff, 0x13, 0xd4, 0x92, 0xf6, 0x6a, + 0x6b, 0xcd, 0x07, 0xf4, 0x12, 0x86, 0x08, 0x6d, + 0x81, 0x93, 0xed, 0x9c, 0xf0, 0xf8, 0x5b, 0xeb, + 0x00, 0x70, 0x7c, 0x40, 0xfa, 0x12, 0x6c, 0xa0, + 0x07, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, + 0xa1, 0x44, 0x03, 0x42, 0x00, 0x04, 0xdf, 0x23, + 0x42, 0xe5, 0xab, 0x3c, 0x25, 0x53, 0x79, 0x32, + 0x31, 0x7d, 0xe6, 0x87, 0xcd, 0x4a, 0x04, 0x41, + 0x55, 0x78, 0xdf, 0xd0, 0x22, 0xad, 0x60, 0x44, + 0x96, 0x7c, 0xf9, 0xe6, 0xbd, 0x3d, 0xe7, 0xf9, + 0xc3, 0x0c, 0x25, 0x40, 0x7d, 0x95, 0x42, 0x5f, + 0x76, 0x41, 0x4d, 0x81, 0xa4, 0x81, 0xec, 0x99, + 0x41, 0xfa, 0x4a, 0xd9, 0x55, 0x55, 0x7c, 0x4f, + 0xb1, 0xd9, 0x41, 0x75, 0x43, 0x44 + }; + string label = "EC PRIVATE KEY"; + Assert.True(PemEncoding.TryWrite(label, data, buffer, out int charsWritten)); + string pem = new string(buffer, 0, charsWritten); + string expected = + "-----BEGIN EC PRIVATE KEY-----\n" + + "MHQCAQEEICBZ7/8T1JL2amvNB/QShghtgZPtnPD4W+sAcHxA+hJsoAcGBSuBBAAK\n" + + "oUQDQgAE3yNC5as8JVN5MjF95ofNSgRBVXjf0CKtYESWfPnmvT3n+cMMJUB9lUJf\n" + + "dkFNgaSB7JlB+krZVVV8T7HZQXVDRA==\n" + + "-----END EC PRIVATE KEY-----"; + Assert.Equal(expected, pem); + } + + [Fact] + public static void Write_EcKey() + { + ReadOnlySpan data = new byte[] { + 0x30, 0x74, 0x02, 0x01, 0x01, 0x04, 0x20, 0x20, + 0x59, 0xef, 0xff, 0x13, 0xd4, 0x92, 0xf6, 0x6a, + 0x6b, 0xcd, 0x07, 0xf4, 0x12, 0x86, 0x08, 0x6d, + 0x81, 0x93, 0xed, 0x9c, 0xf0, 0xf8, 0x5b, 0xeb, + 0x00, 0x70, 0x7c, 0x40, 0xfa, 0x12, 0x6c, 0xa0, + 0x07, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a, + 0xa1, 0x44, 0x03, 0x42, 0x00, 0x04, 0xdf, 0x23, + 0x42, 0xe5, 0xab, 0x3c, 0x25, 0x53, 0x79, 0x32, + 0x31, 0x7d, 0xe6, 0x87, 0xcd, 0x4a, 0x04, 0x41, + 0x55, 0x78, 0xdf, 0xd0, 0x22, 0xad, 0x60, 0x44, + 0x96, 0x7c, 0xf9, 0xe6, 0xbd, 0x3d, 0xe7, 0xf9, + 0xc3, 0x0c, 0x25, 0x40, 0x7d, 0x95, 0x42, 0x5f, + 0x76, 0x41, 0x4d, 0x81, 0xa4, 0x81, 0xec, 0x99, + 0x41, 0xfa, 0x4a, 0xd9, 0x55, 0x55, 0x7c, 0x4f, + 0xb1, 0xd9, 0x41, 0x75, 0x43, 0x44 + }; + string label = "EC PRIVATE KEY"; + char[] result = PemEncoding.Write(label, data); + string pem = new string(result); + string expected = + "-----BEGIN EC PRIVATE KEY-----\n" + + "MHQCAQEEICBZ7/8T1JL2amvNB/QShghtgZPtnPD4W+sAcHxA+hJsoAcGBSuBBAAK\n" + + "oUQDQgAE3yNC5as8JVN5MjF95ofNSgRBVXjf0CKtYESWfPnmvT3n+cMMJUB9lUJf\n" + + "dkFNgaSB7JlB+krZVVV8T7HZQXVDRA==\n" + + "-----END EC PRIVATE KEY-----"; + Assert.Equal(expected, pem); + } + + [Fact] + public static void TryWrite_Throws_InvalidLabel() + { + char[] buffer = new char[50]; + AssertExtensions.Throws("label", () => + PemEncoding.TryWrite("\n", default, buffer, out _)); + } + + [Fact] + public static void Write_Throws_InvalidLabel() + { + AssertExtensions.Throws("label", () => + PemEncoding.Write("\n", default)); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj b/src/libraries/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj index 28af9302b239f..87109c395eaaf 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj @@ -77,6 +77,8 @@ + + CommonTest\System\Security\Cryptography\ByteUtils.cs @@ -84,4 +86,4 @@ Common\System\Memory\PointerMemoryManager.cs - \ No newline at end of file +