From 8cc21eb54184ebb7ba2d242d8ca095645d25c3f4 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 12 Feb 2020 16:26:19 -0500 Subject: [PATCH 01/40] Find / TryFind PEM --- .../System.Security.Cryptography.Encoding.cs | 17 + .../src/Resources/Strings.resx | 3 + ...stem.Security.Cryptography.Encoding.csproj | 4 +- .../Security/Cryptography/PemEncoding.cs | 219 ++++++++++++ .../System/Security/Cryptography/PemFields.cs | 40 +++ .../tests/PemEncodingTests.cs | 312 ++++++++++++++++++ ...ecurity.Cryptography.Encoding.Tests.csproj | 3 +- 7 files changed, 596 insertions(+), 2 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography.Encoding/src/System/Security/Cryptography/PemEncoding.cs create mode 100644 src/libraries/System.Security.Cryptography.Encoding/src/System/Security/Cryptography/PemFields.cs create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs 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 6be980f93bed7f..35116b47630732 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 54f8f7c1f73b05..03830b9f1a06cf 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,9 @@ Value was invalid. + + No PEM encoded data found. + 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 0c8b8432bdb104..a095504756c937 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 00000000000000..ff4c8612a18621 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/src/System/Security/Cryptography/PemEncoding.cs @@ -0,0 +1,219 @@ +// 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 +{ + public static class PemEncoding + { + private const string s_Preeb = "-----BEGIN "; + private const string s_Posteb = "-----END "; + private const string s_Ending = "-----"; + + public static PemFields Find(ReadOnlySpan pemData) + { + if (!TryFind(pemData, out PemFields fields)) + { + throw new ArgumentException(SR.Argument_PemEncoding_NoPemFound, nameof(pemData)); + } + return fields; + } + + 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 < s_Preeb.Length + s_Ending.Length * 2 + s_Posteb.Length) + { + fields = default; + return false; + } + + int preebLinePosition = 0; + while (TryReadNextLine(pemData, ref preebLinePosition, out Range lineRange)) + { + ReadOnlySpan line = pemData[lineRange]; + int preebIndex = line.IndexOf(s_Preeb); + + if (preebIndex == -1 || + (preebIndex > 0 && !line[..preebIndex].IsWhiteSpace())) // can only be preceeded by whitespace + { + continue; + } + + int preebEndingIndex = line[(preebIndex + s_Preeb.Length)..].IndexOf(s_Ending); + + if (preebEndingIndex == -1) + { + continue; + } + + (int preebOffset, _) = lineRange.GetOffsetAndLength(pemData.Length); + int preebStartIndex = preebOffset + preebIndex; + int startLabelIndex = preebStartIndex + s_Preeb.Length; + int endLabelIndex = startLabelIndex + preebEndingIndex; + Range labelRange = startLabelIndex..endLabelIndex; + ReadOnlySpan label = pemData[labelRange]; + + if (!IsValidLabel(label)) + { + continue; + } + + ReadOnlySpan posteb = string.Concat(s_Posteb, label, s_Ending); + Range postebLineRange = lineRange; + int postebLinePosition = preebLinePosition; + + // in lax decoding a posteb may appear on the same line as the preeb. + // start on the current line. We do not need to check that this posteb + // comes after the preeb because the preeb's prior content has already + // been validated to be whitespace. + do + { + ReadOnlySpan postebLine = pemData[postebLineRange]; + int postebIndex = postebLine.IndexOf(posteb); + + if (postebIndex == -1) + { + continue; + } + + (int postebOffset, _) = postebLineRange.GetOffsetAndLength(pemData.Length); + int postebEndIndex = postebOffset + postebIndex + posteb.Length; + Range location = preebStartIndex..postebEndIndex; + Range content = (endLabelIndex + s_Ending.Length)..(postebOffset + postebIndex); + + if (!postebLine[(postebIndex + posteb.Length)..].IsWhiteSpace()) + { + break; + } + + if (IsValidBase64(pemData, content, out Range base64range, out int decodedBase64Size)) + { + fields = new PemFields(labelRange, base64range, location, decodedBase64Size); + return true; + } + break; + } + while (TryReadNextLine(pemData, ref postebLinePosition, out postebLineRange)); + } + + fields = default; + return false; + } + + private static bool IsValidLabel(ReadOnlySpan data) + { + static bool IsLabelChar(char c) => c >= 0x21 && c <= 0x7e && c != '-'; + + if (data.Length == 0) + return true; + + // First character of label must be a labelchar + if (!IsLabelChar(data[0])) + return false; + + for (int index = 1; index < data.Length; index++) + { + char c = data[index]; + if (!IsLabelChar(c) && c != ' ' && c != '-') + { + return false; + } + } + return true; + } + + private static bool IsValidBase64( + ReadOnlySpan data, + Range content, + out Range base64range, + out int decodedSize) + { + static bool IsBase64Char(char c) => + (c >= '0' && c <= '9') || + (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + c == '+' || c == '/' || c == '='; + + int base64chars = 0; + int precedingWhiteSpace = 0; + int trailingWhiteSpace = 0; + (int offset, int length) = content.GetOffsetAndLength(data.Length); + for (int index = offset; index < offset + length; index++) + { + char c = data[index]; + + if (IsBase64Char(c)) + { + trailingWhiteSpace = 0; + base64chars++; + } + else if (c == ' ' || c == '\n' || c == '\r' || + c == '\t' || c == '\v') + { + if (base64chars == 0) + { + precedingWhiteSpace++; + } + else + { + trailingWhiteSpace++; + } + } + else + { + base64range = default; + decodedSize = 0; + return false; + } + } + + base64range = (offset + precedingWhiteSpace)..(offset + (length - trailingWhiteSpace)); + decodedSize = (base64chars * 3) / 4; + return true; + } + + private static bool TryReadNextLine(ReadOnlySpan data, ref int position, out Range nextLineContent) + { + if (position < 0) + { + nextLineContent = default; + return false; + } + + int newLineIndex = data[position..].IndexOfAny('\n', '\r'); + + if (newLineIndex == -1) + { + nextLineContent = position..; + position = -1; + return true; + } + else if (data[newLineIndex] == '\r' && + newLineIndex < data.Length - 1 && + data[newLineIndex + 1] == '\n') + { + // We landed at a Windows new line, we should consume both the \r and \n. + nextLineContent = position..(position + newLineIndex); + position += newLineIndex + 2; + return true; + } + else + { + nextLineContent = position..(position + newLineIndex); + position += newLineIndex + 1; + return true; + } + } + + public static int GetEncodedSize(int labelLength, int dataLength) => + throw new System.NotImplementedException(); + + public static bool TryWrite(ReadOnlySpan label, ReadOnlySpan data, Span destination, out int charsWritten) => + throw new System.NotImplementedException(); + public static char[] Write(ReadOnlySpan label, ReadOnlySpan data) => + throw new System.NotImplementedException(); + } +} 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 00000000000000..7dd9faab26eefe --- /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; + } + + /// + /// The location of the PEM, including the surrounding ecapsulation boundaries. + /// + public Range Location { get; } + + /// + /// The location of the label. + /// + public Range Label { get; } + + /// + /// The location of the base64 data inside of the PEM. + /// + public Range Base64Data { get; } + + /// + /// The size of the decoded base 64, in bytes. + /// + public int DecodedDataLength { get; } + } +} 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 00000000000000..8f06c5407ceebb --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -0,0 +1,312 @@ +// 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 Find_ThrowsWhenNoPem() + { + AssertExtensions.Throws("pemData", + () => PemEncoding.Find(string.Empty)); + } + + [Fact] + public static void Find_Simple() + { + string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + PemFields fields = PemEncoding.Find(content); + 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 static void TryFind_True_Minimum() + { + string content = "-----BEGIN ----------END -----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal(string.Empty, content[fields.Label]); + Assert.Equal(content, content[fields.Location]); + Assert.Equal(string.Empty, content[fields.Base64Data]); + Assert.Equal(0, fields.DecodedDataLength); + } + + [Fact] + public static void TryFind_True_Simple() + { + string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + 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 static void TryFind_True_WindowsStyleEol() + { + string content = "-----BEGIN TEST-----\r\nZm\r\n9v\r\n-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("TEST", content[fields.Label]); + Assert.Equal(content, content[fields.Location]); + Assert.Equal("Zm\r\n9v", content[fields.Base64Data]); + Assert.Equal(3, fields.DecodedDataLength); + } + + + [Fact] + public static void TryFind_True_EolMixed() + { + string content = "-----BEGIN TEST-----\rZm\r\n9v\n-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("TEST", content[fields.Label]); + Assert.Equal(content, content[fields.Location]); + Assert.Equal("Zm\r\n9v", content[fields.Base64Data]); + Assert.Equal(3, fields.DecodedDataLength); + } + + [Fact] + public static void TryFind_True_OneLine() + { + string content = "-----BEGIN TEST-----Zm9v-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + 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 static void TryFind_True_MultiLineBase64() + { + string content = "-----BEGIN TEST-----\nZm\n9v\n-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("TEST", content[fields.Label]); + Assert.Equal(content, content[fields.Location]); + Assert.Equal("Zm\n9v", content[fields.Base64Data]); + Assert.Equal(3, fields.DecodedDataLength); + } + + [Fact] + public static void TryFind_True_PrecedingLines() + { + string content = "boop\n-----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("TEST", content[fields.Label]); + Assert.Equal(content["boop\n".Length..], content[fields.Location]); + Assert.Equal("Zm9v", content[fields.Base64Data]); + Assert.Equal(3, fields.DecodedDataLength); + } + + [Fact] + public static void TryFind_True_PrecedingLines_WindowsEolStyle() + { + string content = "boop\r\n-----BEGIN TEST-----\r\nZm9v\r\n-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("TEST", content[fields.Label]); + Assert.Equal(content["boop\r\n".Length..], content[fields.Location]); + Assert.Equal("Zm9v", content[fields.Base64Data]); + Assert.Equal(3, fields.DecodedDataLength); + } + + [Fact] + public static void TryFind_True_PrecedingLines_ReturnLines() + { + string content = "boop\r-----BEGIN TEST-----\rZm9v\r-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("TEST", content[fields.Label]); + Assert.Equal(content["boop\r".Length..], content[fields.Location]); + Assert.Equal("Zm9v", content[fields.Base64Data]); + Assert.Equal(3, fields.DecodedDataLength); + } + + [Fact] + public static void TryFind_True_PrecedingLinesAndWhitespaceBeforePreeb() + { + string content = "boop\n -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("TEST", content[fields.Label]); + Assert.Equal(content["boop\n ".Length..], content[fields.Location]); + Assert.Equal("Zm9v", content[fields.Base64Data]); + Assert.Equal(3, fields.DecodedDataLength); + } + + [Fact] + public static void TryFind_True_TrailingWhitespaceAfterPosteb() + { + string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST----- "; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("TEST", content[fields.Label]); + Assert.Equal(content[..^" ".Length], content[fields.Location]); + Assert.Equal("Zm9v", content[fields.Base64Data]); + Assert.Equal(3, fields.DecodedDataLength); + } + + [Fact] + public static void TryFind_True_EmptyLabel() + { + string content = "-----BEGIN -----\nZm9v\n-----END -----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal(11..11, fields.Label); + Assert.Equal(content, content[fields.Location]); + Assert.Equal("Zm9v", content[fields.Base64Data]); + Assert.Equal(3, fields.DecodedDataLength); + } + + [Fact] + public static void TryFind_True_EmptyContent_OneLine() + { + string content = "-----BEGIN EMPTY----------END EMPTY-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("EMPTY", content[fields.Label]); + Assert.Equal(content, content[fields.Location]); + Assert.Equal(21..21, fields.Base64Data); + Assert.Equal(0, fields.DecodedDataLength); + } + + [Fact] + public static void TryFind_True_EmptyContent_ManyLinesOfWhitespace() + { + string content = "-----BEGIN EMPTY-----\n\t\n\t\n\t \n-----END EMPTY-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("EMPTY", content[fields.Label]); + Assert.Equal(content, content[fields.Location]); + Assert.Equal(30..30, fields.Base64Data); + Assert.Equal(0, fields.DecodedDataLength); + } + + [Theory] + [InlineData("CERTIFICATE")] + [InlineData("X509 CRL")] + [InlineData("PKCS7")] + [InlineData("PRIVATE KEY")] + [InlineData("RSA PRIVATE KEY")] + public static void TryFind_True_CommonLabels(string label) + { + string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal(label, content[fields.Label]); + } + + [Fact] + public static void TryFind_True_MultiPem() + { + string content = @" +-----BEGIN EC PARAMETERS----- +BgUrgQQACg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHQCAQEEIIpP2qP/mGWDAojQDNrNfUHwYGNPKeO6VLt+POJeCJ3OoAcGBSuBBAAK +oUQDQgAEeDThNbdvTkptgvfNOpETlKBcWDUKs9IcQ/RaFeBntqt+6J875A79YhmD +D7ofwIDcVqzOJQDhSN54EQ17CFQiwg== +-----END EC PRIVATE KEY----- +"; + ReadOnlySpan pem = content; + List labels = new List(); + while (PemEncoding.TryFind(pem, out PemFields fields)) + { + labels.Add(pem[fields.Label].ToString()); + pem = pem[fields.Location.End..]; + } + + Assert.Equal(new string[] { "EC PARAMETERS", "EC PRIVATE KEY" }, labels); + } + + [Fact] + public static void TryFind_True_FindsPemAfterPemWithInvalidBase64() + { + string content = @" +-----BEGIN TEST----- +$$$$ +-----END TEST----- +-----BEGIN TEST2----- +Zm9v +-----END TEST2-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("TEST2", content[fields.Label]); + Assert.Equal("Zm9v", content[fields.Base64Data]); + } + + [Fact] + public static void TryFind_True_FindsPemAfterPemWithInvalidLabel() + { + string content = @" +-----BEGIN ------ +YmFy +-----END ------ +-----BEGIN TEST2----- +Zm9v +-----END TEST2-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("TEST2", content[fields.Label]); + Assert.Equal("Zm9v", content[fields.Base64Data]); + } + + [Fact] + public static void TryFind_False_Empty() + { + Assert.False(PemEncoding.TryFind(string.Empty, out _)); + } + + [Fact] + public static void TryFind_False_PostEbBeforePreEb() + { + string content = "-----END TEST-----\n-----BEGIN TEST-----\nZm9v"; + Assert.False(PemEncoding.TryFind(content, out _)); + } + + [Theory] + [InlineData("\tOOPS")] + [InlineData(" OOPS")] + [InlineData("-OOPS")] + public static void TryFind_False_InvalidLabel(string label) + { + string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; + Assert.False(PemEncoding.TryFind(content, out _)); + } + + [Fact] + public static void TryFind_False_InvalidBase64() + { + string content = "-----BEGIN TEST-----\n$$$$\n-----END TEST-----"; + Assert.False(PemEncoding.TryFind(content, out _)); + } + + [Fact] + public static void TryFind_False_PrecedingLinesAndSignificantCharsBeforePreeb() + { + string content = "boop\nbeep-----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + Assert.False(PemEncoding.TryFind(content, out _)); + } + + [Fact] + public static void TryFind_False_ContentOnPostEbLine() + { + string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST-----boop"; + Assert.False(PemEncoding.TryFind(content, out _)); + } + + [Fact] + public static void TryFind_False_NoPostEncapBoundary() + { + string content = "-----BEGIN TEST-----\nZm9v\n"; + Assert.False(PemEncoding.TryFind(content, out _)); + } + + [Fact] + public static void TryFind_False_IncompletePostEncapBoundary() + { + string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST"; + Assert.False(PemEncoding.TryFind(content, out _)); + } + } +} 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 f9ff764e730b77..51fa35117f4bcd 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 @@ -53,6 +53,7 @@ + CommonTest\System\Security\Cryptography\ByteUtils.cs @@ -60,4 +61,4 @@ Common\System\Memory\PointerMemoryManager.cs - \ No newline at end of file + From 508b09aaf85ec803eabc584e6e7bdc9f7743160c Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 13 Feb 2020 14:28:20 -0500 Subject: [PATCH 02/40] Implement GetEncodedSize. --- .../src/Resources/Strings.resx | 6 ++ .../Security/Cryptography/PemEncoding.cs | 70 +++++++++++++++---- .../tests/PemEncodingTests.cs | 63 +++++++++++++++++ 3 files changed, 126 insertions(+), 13 deletions(-) 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 03830b9f1a06cf..30d98b05d68292 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography.Encoding/src/Resources/Strings.resx @@ -120,6 +120,12 @@ No PEM encoded data found. + + 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/PemEncoding.cs b/src/libraries/System.Security.Cryptography.Encoding/src/System/Security/Cryptography/PemEncoding.cs index ff4c8612a18621..475ac99b0d17bd 100644 --- 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 @@ -6,9 +6,10 @@ namespace System.Security.Cryptography { public static class PemEncoding { - private const string s_Preeb = "-----BEGIN "; - private const string s_Posteb = "-----END "; - private const string s_Ending = "-----"; + private const string Preeb = "-----BEGIN "; + private const string Posteb = "-----END "; + private const string Ending = "-----"; + private const int EncodedLineLength = 64; public static PemFields Find(ReadOnlySpan pemData) { @@ -24,7 +25,7 @@ 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 < s_Preeb.Length + s_Ending.Length * 2 + s_Posteb.Length) + if (pemData.Length < Preeb.Length + Ending.Length * 2 + Posteb.Length) { fields = default; return false; @@ -34,7 +35,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) while (TryReadNextLine(pemData, ref preebLinePosition, out Range lineRange)) { ReadOnlySpan line = pemData[lineRange]; - int preebIndex = line.IndexOf(s_Preeb); + int preebIndex = line.IndexOf(Preeb); if (preebIndex == -1 || (preebIndex > 0 && !line[..preebIndex].IsWhiteSpace())) // can only be preceeded by whitespace @@ -42,7 +43,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) continue; } - int preebEndingIndex = line[(preebIndex + s_Preeb.Length)..].IndexOf(s_Ending); + int preebEndingIndex = line[(preebIndex + Preeb.Length)..].IndexOf(Ending); if (preebEndingIndex == -1) { @@ -51,7 +52,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) (int preebOffset, _) = lineRange.GetOffsetAndLength(pemData.Length); int preebStartIndex = preebOffset + preebIndex; - int startLabelIndex = preebStartIndex + s_Preeb.Length; + int startLabelIndex = preebStartIndex + Preeb.Length; int endLabelIndex = startLabelIndex + preebEndingIndex; Range labelRange = startLabelIndex..endLabelIndex; ReadOnlySpan label = pemData[labelRange]; @@ -61,7 +62,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) continue; } - ReadOnlySpan posteb = string.Concat(s_Posteb, label, s_Ending); + ReadOnlySpan posteb = string.Concat(Posteb, label, Ending); Range postebLineRange = lineRange; int postebLinePosition = preebLinePosition; @@ -82,7 +83,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) (int postebOffset, _) = postebLineRange.GetOffsetAndLength(pemData.Length); int postebEndIndex = postebOffset + postebIndex + posteb.Length; Range location = preebStartIndex..postebEndIndex; - Range content = (endLabelIndex + s_Ending.Length)..(postebOffset + postebIndex); + Range content = (endLabelIndex + Ending.Length)..(postebOffset + postebIndex); if (!postebLine[(postebIndex + posteb.Length)..].IsWhiteSpace()) { @@ -208,11 +209,54 @@ private static bool TryReadNextLine(ReadOnlySpan data, ref int position, o } } - public static int GetEncodedSize(int labelLength, int dataLength) => - throw new System.NotImplementedException(); + 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 = Preeb.Length + labelLength + Ending.Length; + int postebLength = Posteb.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); + lineCount += ((remainder >> 31) - (-remainder >> 31)); //Increment lineCount if remainder is positive. + int encodedDataLengthWithBreaks = encodedDataLength + lineCount; + + if (int.MaxValue - encodedDataLengthWithBreaks < totalEncapLength) + throw new ArgumentException(SR.Argument_PemEncoding_EncodedSizeTooLarge); + + return encodedDataLengthWithBreaks + totalEncapLength; + } + + public static bool TryWrite(ReadOnlySpan label, ReadOnlySpan data, Span destination, out int charsWritten) + { + charsWritten = 0; + return false; + } - public static bool TryWrite(ReadOnlySpan label, ReadOnlySpan data, Span destination, out int charsWritten) => - throw new System.NotImplementedException(); public static char[] Write(ReadOnlySpan label, ReadOnlySpan data) => throw new System.NotImplementedException(); } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs index 8f06c5407ceebb..eade213bcf9ee5 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -308,5 +308,68 @@ public static void TryFind_False_IncompletePostEncapBoundary() string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST"; Assert.False(PemEncoding.TryFind(content, out _)); } + + [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)); + } } } From 0b628d08f9888f069054ab26dc77f6fb172a68d7 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 13 Feb 2020 17:20:12 -0500 Subject: [PATCH 03/40] Added writing and complete doc-comments. --- .../Security/Cryptography/PemEncoding.cs | 170 +++++++++++++++++- .../System/Security/Cryptography/PemFields.cs | 2 +- .../tests/PemEncodingTests.cs | 153 ++++++++++++++++ 3 files changed, 321 insertions(+), 4 deletions(-) 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 index 475ac99b0d17bd..5c79062578893c 100644 --- 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 @@ -2,8 +2,13 @@ // 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; + namespace System.Security.Cryptography { + /// + /// RFC-7468 PEM (Privacy-Enhanced Mail) parsing and encoding. + /// public static class PemEncoding { private const string Preeb = "-----BEGIN "; @@ -11,6 +16,19 @@ public static class PemEncoding private const string Ending = "-----"; private const int EncodedLineLength = 64; + /// + /// Finds the first PEM-encoded data. + /// + /// + /// A span containing the PEM encoded data. + /// + /// + /// does not contain a well-formed PEM encoded value. + /// + /// + /// A structure that contains the location, label, and + /// data location of the encoded data. + /// public static PemFields Find(ReadOnlySpan pemData) { if (!TryFind(pemData, out PemFields fields)) @@ -20,6 +38,19 @@ public static PemFields Find(ReadOnlySpan pemData) return fields; } + /// + /// Finds the first PEM-encoded data. + /// + /// + /// A span containing the PEM encoded data. + /// + /// + /// When this method returns true, the found structure + /// that contains the location, label, and data location of the encoded data. + /// + /// + /// true if PEM encoded data was found; otherwise false. + /// public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) { // Check for the minimum possible encoded length of a PEM structure @@ -209,6 +240,37 @@ private static bool TryReadNextLine(ReadOnlySpan data, ref int position, o } } + /// + /// Given the length of a label and binary data, determines the + /// length of an encoded PEM in characters. + /// + /// + /// 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 PEM is too large to encode in a signed 32-bit integer. + /// public static int GetEncodedSize(int labelLength, int dataLength) { // The largest possible label is MaxLabelSize - when included in the posteb @@ -251,13 +313,115 @@ public static int GetEncodedSize(int labelLength, int dataLength) return encodedDataLengthWithBreaks + totalEncapLength; } + /// + /// Tries to write PEM encoded data to . + /// + /// + /// The label to encode. + /// + /// + /// The data to encode. + /// + /// + /// The destination to write the PEM encoded data to. + /// + /// + /// When this method returns true, this parameter contains the number of characters + /// written to the buffer. + /// + /// + /// true if the buffer is large enough to contain + /// the encoded PEM, otherwise false. + /// + /// + /// 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. + /// public static bool TryWrite(ReadOnlySpan label, ReadOnlySpan data, Span destination, out int charsWritten) { + static void Write(ReadOnlySpan str, Span dest, ref int offset) + { + str.CopyTo(dest[offset..]); + offset += str.Length; + } + + static void WriteBase64(ReadOnlySpan bytes, Span dest, ref int offset) + { + bool success = Convert.TryToBase64Chars(bytes, dest[offset..], out int base64Written); + + if (!success) + throw new CryptographicException(); + + offset += base64Written; + } + + const string NewLine = "\n"; + int encodedSize = GetEncodedSize(label.Length, data.Length); + + if (destination.Length < encodedSize) + { + charsWritten = 0; + return false; + } + charsWritten = 0; - return false; + Write(Preeb, destination, ref charsWritten); + Write(label, destination, ref charsWritten); + Write(Ending, destination, ref charsWritten); + Write(NewLine, destination, ref charsWritten); + + ReadOnlySpan remainingData = data; + while (remainingData.Length >= 48) + { + WriteBase64(remainingData[..48], destination, ref charsWritten); + remainingData = remainingData[48..]; + Write(NewLine, destination, ref charsWritten); + } + + Debug.Assert(remainingData.Length < 48); + + if (remainingData.Length > 0) + { + WriteBase64(remainingData, destination, ref charsWritten); + Write(NewLine, destination, ref charsWritten); + remainingData = default; + } + + Write(Posteb, destination, ref charsWritten); + Write(label, destination, ref charsWritten); + Write(Ending, destination, ref charsWritten); + + return true; } - public static char[] Write(ReadOnlySpan label, ReadOnlySpan data) => - throw new System.NotImplementedException(); + /// + /// 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. + /// + public static char[] Write(ReadOnlySpan label, ReadOnlySpan data) + { + int encodedSize = GetEncodedSize(label.Length, data.Length); + char[] buffer = new char[encodedSize]; + + if (!TryWrite(label, data, buffer, out int charsWritten)) + { + throw new CryptographicException(); + } + + 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 index 7dd9faab26eefe..71fffc83d04e05 100644 --- 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 @@ -33,7 +33,7 @@ internal PemFields(Range label, Range base64data, Range location, int decodedDat public Range Base64Data { get; } /// - /// The size of the decoded base 64, in bytes. + /// The size of the decoded base-64, in bytes. /// public int DecodedDataLength { get; } } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs index eade213bcf9ee5..e7a66bd84b5a10 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -371,5 +371,158 @@ 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 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 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 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 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_Empty() + { + char[] result = PemEncoding.Write(default, default); + Assert.Equal("-----BEGIN -----\n-----END -----", result); + } + + [Fact] + public static void Write_Simple() + { + 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 expected = + "-----BEGIN FANCY DATA-----\n" + + "AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYH\n" + + "-----END FANCY DATA-----"; + Assert.Equal(expected, result); + } } } From b9c418fd842f29c782ad37d384dcc2c0b9991a4c Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 13 Feb 2020 22:19:03 -0500 Subject: [PATCH 04/40] Avoid an allocation for sensible label sizes. --- .../src/System/Security/Cryptography/PemEncoding.cs | 8 +++++++- .../tests/PemEncodingTests.cs | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) 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 index 5c79062578893c..a66b92319892b7 100644 --- 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 @@ -93,7 +93,13 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) continue; } - ReadOnlySpan posteb = string.Concat(Posteb, label, Ending); + int postebLength = Posteb.Length + label.Length + Ending.Length; + Span postebBuffer = postebLength > 256 ? new char[postebLength] : stackalloc char[256]; + Posteb.AsSpan().CopyTo(postebBuffer); + label.CopyTo(postebBuffer[Posteb.Length..]); + Ending.AsSpan().CopyTo(postebBuffer[(Posteb.Length + label.Length)..]); + ReadOnlySpan posteb = postebBuffer[..postebLength]; + Range postebLineRange = lineRange; int postebLinePosition = preebLinePosition; diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs index e7a66bd84b5a10..09b5b667001797 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -28,6 +28,17 @@ public static void Find_Simple() Assert.Equal(3, fields.DecodedDataLength); } + [Fact] + public static void Find_LargeLabel() + { + string label = new string('A', 275); + string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; + PemFields fields = PemEncoding.Find(content); + Assert.Equal(label, content[fields.Label]); + Assert.Equal(content, content[fields.Location]); + Assert.Equal("Zm9v", content[fields.Base64Data]); + Assert.Equal(3, fields.DecodedDataLength); + } [Fact] public static void TryFind_True_Minimum() From fb4e77c037af061a29a61fcfa0ee806268be57d1 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 14 Feb 2020 07:54:12 -0500 Subject: [PATCH 05/40] Stackalloc outside loop once. --- .../src/System/Security/Cryptography/PemEncoding.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index a66b92319892b7..d8ef6235715b47 100644 --- 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 @@ -62,6 +62,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) return false; } + Span postebShortBuffer = stackalloc char[256]; int preebLinePosition = 0; while (TryReadNextLine(pemData, ref preebLinePosition, out Range lineRange)) { @@ -94,7 +95,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) } int postebLength = Posteb.Length + label.Length + Ending.Length; - Span postebBuffer = postebLength > 256 ? new char[postebLength] : stackalloc char[256]; + Span postebBuffer = postebLength > 256 ? new char[postebLength] : postebShortBuffer; Posteb.AsSpan().CopyTo(postebBuffer); label.CopyTo(postebBuffer[Posteb.Length..]); Ending.AsSpan().CopyTo(postebBuffer[(Posteb.Length + label.Length)..]); From 796512719ae415851558c780488e5024ce01fd84 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 14 Feb 2020 11:36:05 -0500 Subject: [PATCH 06/40] Fix Windows line handling. --- .../src/System/Security/Cryptography/PemEncoding.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 index d8ef6235715b47..ef68634cd12843 100644 --- 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 @@ -222,7 +222,8 @@ private static bool TryReadNextLine(ReadOnlySpan data, ref int position, o return false; } - int newLineIndex = data[position..].IndexOfAny('\n', '\r'); + ReadOnlySpan content = data[position..]; + int newLineIndex = content.IndexOfAny('\n', '\r'); if (newLineIndex == -1) { @@ -230,9 +231,9 @@ private static bool TryReadNextLine(ReadOnlySpan data, ref int position, o position = -1; return true; } - else if (data[newLineIndex] == '\r' && - newLineIndex < data.Length - 1 && - data[newLineIndex + 1] == '\n') + else if (content[newLineIndex] == '\r' && + newLineIndex < content.Length - 1 && + content[newLineIndex + 1] == '\n') { // We landed at a Windows new line, we should consume both the \r and \n. nextLineContent = position..(position + newLineIndex); From 558a9370cf284b7057d2f38b881a8603460f1f38 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 19 Feb 2020 09:58:23 -0500 Subject: [PATCH 07/40] Code review feedback. --- .../System/Security/Cryptography/PemEncoding.cs | 14 ++++++++------ .../tests/PemEncodingTests.cs | 11 ++++++++++- 2 files changed, 18 insertions(+), 7 deletions(-) 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 index ef68634cd12843..4829e46423268b 100644 --- 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 @@ -39,7 +39,7 @@ public static PemFields Find(ReadOnlySpan pemData) } /// - /// Finds the first PEM-encoded data. + /// Attempts to find the first PEM-encoded data. /// /// /// A span containing the PEM encoded data. @@ -144,19 +144,21 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) private static bool IsValidLabel(ReadOnlySpan data) { - static bool IsLabelChar(char c) => c >= 0x21 && c <= 0x7e && c != '-'; + static bool IsInRange(char c, char min) => (uint)(c - min) <= (uint)('\x7E' - min); if (data.Length == 0) return true; - // First character of label must be a labelchar - if (!IsLabelChar(data[0])) + // First character of label must be a labelchar, which is a character + // in 0x21..0x7e (both inclusive), except hyphens. + char firstChar = data[0]; + if (!IsInRange(firstChar, '\x21') || firstChar == '-') return false; for (int index = 1; index < data.Length; index++) { - char c = data[index]; - if (!IsLabelChar(c) && c != ' ' && c != '-') + // Characters after the first are permitted to be spaces and hyphens + if (!IsInRange(data[index], '\x20')) { return false; } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs index 09b5b667001797..c578bc185f8e31 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -73,7 +73,6 @@ public static void TryFind_True_WindowsStyleEol() Assert.Equal(3, fields.DecodedDataLength); } - [Fact] public static void TryFind_True_EolMixed() { @@ -208,6 +207,14 @@ public static void TryFind_True_CommonLabels(string label) Assert.Equal(label, content[fields.Label]); } + [Fact] + public static void TryFind_True_LabelCharacterBoundaries() + { + string content = $"-----BEGIN !PANIC~~~-----\nAHHH\n-----END !PANIC~~~-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal("!PANIC~~~", content[fields.Label]); + } + [Fact] public static void TryFind_True_MultiPem() { @@ -279,6 +286,8 @@ public static void TryFind_False_PostEbBeforePreEb() [InlineData("\tOOPS")] [InlineData(" OOPS")] [InlineData("-OOPS")] + [InlineData("te\x7fst")] + [InlineData("te\x19st")] public static void TryFind_False_InvalidLabel(string label) { string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; From 8d8c34b2b96affdf20dd79deec44efd6f8fbca2e Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 19 Feb 2020 10:10:49 -0500 Subject: [PATCH 08/40] Document value of PemFields. --- .../System/Security/Cryptography/PemFields.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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 index 71fffc83d04e05..7b328ada0f03f4 100644 --- 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 @@ -20,21 +20,35 @@ internal PemFields(Range label, Range base64data, Range location, int decodedDat /// /// The location of the PEM, including the surrounding ecapsulation boundaries. /// + /// + /// A marking the locating inside of the data where + /// the PEM was found. + /// public Range Location { get; } /// /// The location of the label. /// + /// + /// A marking the locating of the label. + /// public Range Label { get; } /// /// The location of the base64 data inside of the PEM. /// + /// + /// A marking the locating of the base64 data, + /// excluding leading and trailing whitespace. + /// public Range Base64Data { get; } /// - /// The size of the decoded base-64, in bytes. + /// The size of the decoded base64, in bytes. /// + /// + /// When decoded, the size of the base64 data in bytes. + /// public int DecodedDataLength { get; } } } From 69dfb8034d5d3a0efe873bbaeafac647dde694fe Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 19 Feb 2020 10:15:45 -0500 Subject: [PATCH 09/40] Document exception for Write and TryWrite. --- .../Security/Cryptography/PemEncoding.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 index 4829e46423268b..061f71374be351 100644 --- 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 @@ -347,6 +347,16 @@ public static int GetEncodedSize(int labelLength, int dataLength) /// 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 PEM is too large to possibly encode. + /// public static bool TryWrite(ReadOnlySpan label, ReadOnlySpan data, Span destination, out int charsWritten) { static void Write(ReadOnlySpan str, Span dest, ref int offset) @@ -420,6 +430,16 @@ static void WriteBase64(ReadOnlySpan bytes, Span dest, ref int offse /// 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 PEM is too large to possibly encode. + /// public static char[] Write(ReadOnlySpan label, ReadOnlySpan data) { int encodedSize = GetEncodedSize(label.Length, data.Length); From 13beb43d9dc603363323c2fcbfdb192bc8f661ce Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 19 Feb 2020 11:12:56 -0500 Subject: [PATCH 10/40] whitespace => white space --- .../src/System/Security/Cryptography/PemEncoding.cs | 4 ++-- .../src/System/Security/Cryptography/PemFields.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 index 061f71374be351..23d1443bbd939b 100644 --- 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 @@ -70,7 +70,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) int preebIndex = line.IndexOf(Preeb); if (preebIndex == -1 || - (preebIndex > 0 && !line[..preebIndex].IsWhiteSpace())) // can only be preceeded by whitespace + (preebIndex > 0 && !line[..preebIndex].IsWhiteSpace())) // can only be preceeded by white space { continue; } @@ -107,7 +107,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) // in lax decoding a posteb may appear on the same line as the preeb. // start on the current line. We do not need to check that this posteb // comes after the preeb because the preeb's prior content has already - // been validated to be whitespace. + // been validated to be white space. do { ReadOnlySpan postebLine = pemData[postebLineRange]; 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 index 7b328ada0f03f4..1090c104c31078 100644 --- 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 @@ -39,7 +39,7 @@ internal PemFields(Range label, Range base64data, Range location, int decodedDat /// /// /// A marking the locating of the base64 data, - /// excluding leading and trailing whitespace. + /// excluding leading and trailing white space. /// public Range Base64Data { get; } From 4ee4b1c2260c6c011bbe919e71396f450f01e959 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 19 Feb 2020 16:44:34 -0500 Subject: [PATCH 11/40] Rewrite in terms without lines. --- .../Security/Cryptography/PemEncoding.cs | 174 ++++++++---------- 1 file changed, 77 insertions(+), 97 deletions(-) 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 index 23d1443bbd939b..bd2c83d084262a 100644 --- 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 @@ -11,8 +11,8 @@ namespace System.Security.Cryptography /// public static class PemEncoding { - private const string Preeb = "-----BEGIN "; - private const string Posteb = "-----END "; + private const string PreEBPrefix = "-----BEGIN "; + private const string PostEBPrefix = "-----END "; private const string Ending = "-----"; private const int EncodedLineLength = 64; @@ -56,86 +56,99 @@ 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 < Preeb.Length + Ending.Length * 2 + Posteb.Length) + if (pemData.Length < PreEBPrefix.Length + Ending.Length * 2 + PostEBPrefix.Length) { fields = default; return false; } - Span postebShortBuffer = stackalloc char[256]; - int preebLinePosition = 0; - while (TryReadNextLine(pemData, ref preebLinePosition, out Range lineRange)) + const int PostebStackBufferSize = 256; + Span postebStackBuffer = stackalloc char[PostebStackBufferSize]; + int areaOffset = 0; + int preebIndex; + ReadOnlySpan pemArea = pemData; + while ((preebIndex = pemArea.IndexOf(PreEBPrefix)) >= 0) { - ReadOnlySpan line = pemData[lineRange]; - int preebIndex = line.IndexOf(Preeb); + int labelStartIndex = preebIndex + PreEBPrefix.Length; + int preebIndexInFullData = preebIndex + areaOffset; - if (preebIndex == -1 || - (preebIndex > 0 && !line[..preebIndex].IsWhiteSpace())) // can only be preceeded by white space + if (preebIndexInFullData > 0 && + !char.IsWhiteSpace(pemData[preebIndexInFullData - 1])) { + Debug.Assert(labelStartIndex > 0); + areaOffset += labelStartIndex; + pemArea = pemArea[labelStartIndex..]; continue; } - int preebEndingIndex = line[(preebIndex + Preeb.Length)..].IndexOf(Ending); + int preebEndIndex = pemArea[labelStartIndex..].IndexOf(Ending); - if (preebEndingIndex == -1) + if (preebEndIndex < 0) { - continue; + fields = default; + return false; } - (int preebOffset, _) = lineRange.GetOffsetAndLength(pemData.Length); - int preebStartIndex = preebOffset + preebIndex; - int startLabelIndex = preebStartIndex + Preeb.Length; - int endLabelIndex = startLabelIndex + preebEndingIndex; - Range labelRange = startLabelIndex..endLabelIndex; - ReadOnlySpan label = pemData[labelRange]; + int labelEndingIndex = labelStartIndex + preebEndIndex; + int contentStartIndex = labelEndingIndex + Ending.Length; + ReadOnlySpan label = pemArea[labelStartIndex..labelEndingIndex]; + // There could be a preeb that is valid after this one if it has an invalid + // label, so move from there. if (!IsValidLabel(label)) { + Debug.Assert(labelEndingIndex > 0); + areaOffset += labelEndingIndex; + pemArea = pemArea[labelEndingIndex..]; continue; } - int postebLength = Posteb.Length + label.Length + Ending.Length; - Span postebBuffer = postebLength > 256 ? new char[postebLength] : postebShortBuffer; - Posteb.AsSpan().CopyTo(postebBuffer); - label.CopyTo(postebBuffer[Posteb.Length..]); - Ending.AsSpan().CopyTo(postebBuffer[(Posteb.Length + label.Length)..]); + Range labelRange = (areaOffset + labelStartIndex)..(areaOffset + labelEndingIndex); + int postebLength = PostEBPrefix.Length + label.Length + Ending.Length; + Span postebBuffer = postebLength > PostebStackBufferSize ? new char[postebLength] : postebStackBuffer; + PostEBPrefix.AsSpan().CopyTo(postebBuffer); + label.CopyTo(postebBuffer[PostEBPrefix.Length..]); + Ending.AsSpan().CopyTo(postebBuffer[(PostEBPrefix.Length + label.Length)..]); ReadOnlySpan posteb = postebBuffer[..postebLength]; + int postebStartIndex = pemArea[contentStartIndex..].IndexOf(posteb); - Range postebLineRange = lineRange; - int postebLinePosition = preebLinePosition; - - // in lax decoding a posteb may appear on the same line as the preeb. - // start on the current line. We do not need to check that this posteb - // comes after the preeb because the preeb's prior content has already - // been validated to be white space. - do + if (postebStartIndex < 0) { - ReadOnlySpan postebLine = pemData[postebLineRange]; - int postebIndex = postebLine.IndexOf(posteb); + Debug.Assert(labelEndingIndex > 0); + areaOffset += labelEndingIndex; + pemArea = pemArea[labelEndingIndex..]; + continue; + } - if (postebIndex == -1) - { - continue; - } + int contentEndIndex = postebStartIndex + contentStartIndex; + int pemEndIndex = contentEndIndex + postebLength; - (int postebOffset, _) = postebLineRange.GetOffsetAndLength(pemData.Length); - int postebEndIndex = postebOffset + postebIndex + posteb.Length; - Range location = preebStartIndex..postebEndIndex; - Range content = (endLabelIndex + Ending.Length)..(postebOffset + postebIndex); + if (pemEndIndex < pemArea.Length - 1 && + !char.IsWhiteSpace(pemArea[pemEndIndex])) + { + Debug.Assert(labelEndingIndex > 0); + areaOffset += labelEndingIndex; + pemArea = pemArea[labelEndingIndex..]; + continue; + } - if (!postebLine[(postebIndex + posteb.Length)..].IsWhiteSpace()) - { - break; - } + Range contentRange = (areaOffset + contentStartIndex)..(areaOffset + contentEndIndex); - if (IsValidBase64(pemData, content, out Range base64range, out int decodedBase64Size)) - { - fields = new PemFields(labelRange, base64range, location, decodedBase64Size); - return true; - } - break; + if (!IsValidBase64(pemArea[contentStartIndex..contentEndIndex], + out int base64start, + out int base64end, + out int decodedSize)) + { + Debug.Assert(labelEndingIndex > 0); + areaOffset += labelEndingIndex; + pemArea = pemArea[labelEndingIndex..]; + continue; } - while (TryReadNextLine(pemData, ref postebLinePosition, out postebLineRange)); + + Range pemRange = (areaOffset + preebIndex)..(areaOffset + pemEndIndex); + Range base64range = (contentStartIndex + base64start + areaOffset)..(contentEndIndex + base64end + areaOffset); + fields = new PemFields(labelRange, base64range, pemRange, decodedSize); + return true; } fields = default; @@ -168,8 +181,8 @@ private static bool IsValidLabel(ReadOnlySpan data) private static bool IsValidBase64( ReadOnlySpan data, - Range content, - out Range base64range, + out int base64Start, + out int base64End, out int decodedSize) { static bool IsBase64Char(char c) => @@ -181,8 +194,7 @@ static bool IsBase64Char(char c) => int base64chars = 0; int precedingWhiteSpace = 0; int trailingWhiteSpace = 0; - (int offset, int length) = content.GetOffsetAndLength(data.Length); - for (int index = offset; index < offset + length; index++) + for (int index = 0; index < data.Length; index++) { char c = data[index]; @@ -205,51 +217,19 @@ static bool IsBase64Char(char c) => } else { - base64range = default; + base64Start = default; + base64End = default; decodedSize = 0; return false; } } - base64range = (offset + precedingWhiteSpace)..(offset + (length - trailingWhiteSpace)); + base64Start = precedingWhiteSpace; + base64End = -trailingWhiteSpace; decodedSize = (base64chars * 3) / 4; return true; } - private static bool TryReadNextLine(ReadOnlySpan data, ref int position, out Range nextLineContent) - { - if (position < 0) - { - nextLineContent = default; - return false; - } - - ReadOnlySpan content = data[position..]; - int newLineIndex = content.IndexOfAny('\n', '\r'); - - if (newLineIndex == -1) - { - nextLineContent = position..; - position = -1; - return true; - } - else if (content[newLineIndex] == '\r' && - newLineIndex < content.Length - 1 && - content[newLineIndex + 1] == '\n') - { - // We landed at a Windows new line, we should consume both the \r and \n. - nextLineContent = position..(position + newLineIndex); - position += newLineIndex + 2; - return true; - } - else - { - nextLineContent = position..(position + newLineIndex); - position += newLineIndex + 1; - return true; - } - } - /// /// Given the length of a label and binary data, determines the /// length of an encoded PEM in characters. @@ -307,8 +287,8 @@ public static int GetEncodedSize(int labelLength, int dataLength) if (dataLength > MaxDataLength) throw new ArgumentOutOfRangeException(nameof(dataLength), SR.Argument_PemEncoding_EncodedSizeTooLarge); - int preebLength = Preeb.Length + labelLength + Ending.Length; - int postebLength = Posteb.Length + labelLength + Ending.Length; + 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 @@ -385,7 +365,7 @@ static void WriteBase64(ReadOnlySpan bytes, Span dest, ref int offse } charsWritten = 0; - Write(Preeb, destination, ref charsWritten); + Write(PreEBPrefix, destination, ref charsWritten); Write(label, destination, ref charsWritten); Write(Ending, destination, ref charsWritten); Write(NewLine, destination, ref charsWritten); @@ -407,7 +387,7 @@ static void WriteBase64(ReadOnlySpan bytes, Span dest, ref int offse remainingData = default; } - Write(Posteb, destination, ref charsWritten); + Write(PostEBPrefix, destination, ref charsWritten); Write(label, destination, ref charsWritten); Write(Ending, destination, ref charsWritten); From 2362abfdf6a893684f4d245b52b3c9cc2c4bdb2b Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 19 Feb 2020 16:53:50 -0500 Subject: [PATCH 12/40] Minor refactoring --- .../Security/Cryptography/PemEncoding.cs | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) 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 index bd2c83d084262a..30672b8d82556e 100644 --- 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 @@ -62,8 +62,8 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) return false; } - const int PostebStackBufferSize = 256; - Span postebStackBuffer = stackalloc char[PostebStackBufferSize]; + const int postebStackBufferSize = 256; + Span postebStackBuffer = stackalloc char[postebStackBufferSize]; int areaOffset = 0; int preebIndex; ReadOnlySpan pemArea = pemData; @@ -97,15 +97,12 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) // label, so move from there. if (!IsValidLabel(label)) { - Debug.Assert(labelEndingIndex > 0); - areaOffset += labelEndingIndex; - pemArea = pemArea[labelEndingIndex..]; - continue; + goto next_after_label; } Range labelRange = (areaOffset + labelStartIndex)..(areaOffset + labelEndingIndex); int postebLength = PostEBPrefix.Length + label.Length + Ending.Length; - Span postebBuffer = postebLength > PostebStackBufferSize ? new char[postebLength] : postebStackBuffer; + Span postebBuffer = postebLength > postebStackBufferSize ? new char[postebLength] : postebStackBuffer; PostEBPrefix.AsSpan().CopyTo(postebBuffer); label.CopyTo(postebBuffer[PostEBPrefix.Length..]); Ending.AsSpan().CopyTo(postebBuffer[(PostEBPrefix.Length + label.Length)..]); @@ -114,10 +111,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) if (postebStartIndex < 0) { - Debug.Assert(labelEndingIndex > 0); - areaOffset += labelEndingIndex; - pemArea = pemArea[labelEndingIndex..]; - continue; + goto next_after_label; } int contentEndIndex = postebStartIndex + contentStartIndex; @@ -126,10 +120,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) if (pemEndIndex < pemArea.Length - 1 && !char.IsWhiteSpace(pemArea[pemEndIndex])) { - Debug.Assert(labelEndingIndex > 0); - areaOffset += labelEndingIndex; - pemArea = pemArea[labelEndingIndex..]; - continue; + goto next_after_label; } Range contentRange = (areaOffset + contentStartIndex)..(areaOffset + contentEndIndex); @@ -149,6 +140,11 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) Range base64range = (contentStartIndex + base64start + areaOffset)..(contentEndIndex + base64end + areaOffset); fields = new PemFields(labelRange, base64range, pemRange, decodedSize); return true; + + next_after_label: + Debug.Assert(labelEndingIndex > 0); + areaOffset += labelEndingIndex; + pemArea = pemArea[labelEndingIndex..]; } fields = default; From 2745dfa193ab80a88ca940febc6281c01714ada3 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 19 Feb 2020 17:03:38 -0500 Subject: [PATCH 13/40] Add test for mismatched labels --- .../Security/Cryptography/PemEncoding.cs | 10 +++---- .../tests/PemEncodingTests.cs | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) 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 index 30672b8d82556e..ee8b1eb337692b 100644 --- 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 @@ -11,9 +11,9 @@ namespace System.Security.Cryptography /// public static class PemEncoding { - private const string PreEBPrefix = "-----BEGIN "; - private const string PostEBPrefix = "-----END "; - private const string Ending = "-----"; + private static readonly ReadOnlySpan s_PreEBPrefix = "-----BEGIN "; + private static readonly ReadOnlySpan s_PostEBPrefix = "-----END "; + private static readonly ReadOnlySpan s_Ending = "-----"; private const int EncodedLineLength = 64; /// @@ -103,9 +103,9 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) Range labelRange = (areaOffset + labelStartIndex)..(areaOffset + labelEndingIndex); int postebLength = PostEBPrefix.Length + label.Length + Ending.Length; Span postebBuffer = postebLength > postebStackBufferSize ? new char[postebLength] : postebStackBuffer; - PostEBPrefix.AsSpan().CopyTo(postebBuffer); + PostEBPrefix.CopyTo(postebBuffer); label.CopyTo(postebBuffer[PostEBPrefix.Length..]); - Ending.AsSpan().CopyTo(postebBuffer[(PostEBPrefix.Length + label.Length)..]); + Ending.CopyTo(postebBuffer[(PostEBPrefix.Length + label.Length)..]); ReadOnlySpan posteb = postebBuffer[..postebLength]; int postebStartIndex = pemArea[contentStartIndex..].IndexOf(posteb); diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs index c578bc185f8e31..5a9efedba2229d 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -28,6 +28,27 @@ public static void Find_Simple() Assert.Equal(3, fields.DecodedDataLength); } + [Fact] + public static void TryFind_True_IncompletePreebPrefixed() + { + string content = "-----BEGIN FAIL -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out _)); + } + + [Fact] + public static void TryFind_True_CompletePreebPrefixedDifferentLabel() + { + string content = "-----BEGIN FAIL----- -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out _)); + } + + [Fact] + public static void TryFind_True_CompletePreebPrefixedSameLabel() + { + string content = "-----BEGIN TEST----- -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out _)); + } + [Fact] public static void Find_LargeLabel() { @@ -315,6 +336,13 @@ public static void TryFind_False_ContentOnPostEbLine() Assert.False(PemEncoding.TryFind(content, out _)); } + [Fact] + public static void TryFind_False_MismatchedLabels() + { + string content = "-----BEGIN TEST-----\nZm9v\n-----END FAIL-----"; + Assert.False(PemEncoding.TryFind(content, out _)); + } + [Fact] public static void TryFind_False_NoPostEncapBoundary() { From 0c7171110ee8ece2e5d27766126a6fb797a1f294 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 20 Feb 2020 10:10:49 -0500 Subject: [PATCH 14/40] Fix label validation. --- .../Security/Cryptography/PemEncoding.cs | 44 +++++++++++++------ .../tests/PemEncodingTests.cs | 14 ++++++ 2 files changed, 45 insertions(+), 13 deletions(-) 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 index ee8b1eb337692b..77aa922e10f846 100644 --- 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 @@ -11,9 +11,9 @@ namespace System.Security.Cryptography /// public static class PemEncoding { - private static readonly ReadOnlySpan s_PreEBPrefix = "-----BEGIN "; - private static readonly ReadOnlySpan s_PostEBPrefix = "-----END "; - private static readonly ReadOnlySpan s_Ending = "-----"; + private const string PreEBPrefix = "-----BEGIN "; + private const string PostEBPrefix = "-----END "; + private const string Ending = "-----"; private const int EncodedLineLength = 64; /// @@ -103,9 +103,9 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) Range labelRange = (areaOffset + labelStartIndex)..(areaOffset + labelEndingIndex); int postebLength = PostEBPrefix.Length + label.Length + Ending.Length; Span postebBuffer = postebLength > postebStackBufferSize ? new char[postebLength] : postebStackBuffer; - PostEBPrefix.CopyTo(postebBuffer); + PostEBPrefix.AsSpan().CopyTo(postebBuffer); label.CopyTo(postebBuffer[PostEBPrefix.Length..]); - Ending.CopyTo(postebBuffer[(PostEBPrefix.Length + label.Length)..]); + Ending.AsSpan().CopyTo(postebBuffer[(PostEBPrefix.Length + label.Length)..]); ReadOnlySpan posteb = postebBuffer[..postebLength]; int postebStartIndex = pemArea[contentStartIndex..].IndexOf(posteb); @@ -142,7 +142,14 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) return true; next_after_label: - Debug.Assert(labelEndingIndex > 0); + if (labelEndingIndex == 0) + { + // We somehow ended up in a situation where we will advance 0 characters, which means we'll + // we'll probably end up here again in a loop. To avoid getting stuck in a loop, detect this + // situation and return. + fields = default; + return false; + } areaOffset += labelEndingIndex; pemArea = pemArea[labelEndingIndex..]; } @@ -153,24 +160,35 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) private static bool IsValidLabel(ReadOnlySpan data) { - static bool IsInRange(char c, char min) => (uint)(c - min) <= (uint)('\x7E' - min); + static bool IsLabelChar(char c) => (uint)(c - 0x21u) <= 0x5du && c != '-'; - if (data.Length == 0) + // Empty labels are permitted per RFC 7468. + if (data.IsEmpty) return true; // First character of label must be a labelchar, which is a character // in 0x21..0x7e (both inclusive), except hyphens. - char firstChar = data[0]; - if (!IsInRange(firstChar, '\x21') || firstChar == '-') + if (!IsLabelChar(data[0])) return false; + bool previousSpaceOrHyphen = false; for (int index = 1; index < data.Length; index++) { - // Characters after the first are permitted to be spaces and hyphens - if (!IsInRange(data[index], '\x20')) + char c = data[index]; + + if (IsLabelChar(c)) { - return false; + previousSpaceOrHyphen = false; + continue; } + + bool isSpaceOrHyphen = c == ' ' || c == '-'; + + if (!isSpaceOrHyphen || previousSpaceOrHyphen) + return false; + + previousSpaceOrHyphen = true; + } return true; } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs index 5a9efedba2229d..aace2dd019a8db 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -228,6 +228,18 @@ public static void TryFind_True_CommonLabels(string label) Assert.Equal(label, content[fields.Label]); } + [Theory] + [InlineData("H E L L O")] + [InlineData("H-E-L-L-O")] + [InlineData("H-E-L-L-O ")] + [InlineData("HEL-LO")] + public static void TryFind_True_LabelsWithHyphenSpace(string label) + { + string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal(label, content[fields.Label]); + } + [Fact] public static void TryFind_True_LabelCharacterBoundaries() { @@ -309,6 +321,8 @@ public static void TryFind_False_PostEbBeforePreEb() [InlineData("-OOPS")] [InlineData("te\x7fst")] [InlineData("te\x19st")] + [InlineData("te st")] //two spaces + [InlineData("te- st")] public static void TryFind_False_InvalidLabel(string label) { string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; From 8107961fb150461800b1266eaf239c7d97a36cde Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 20 Feb 2020 14:26:24 -0500 Subject: [PATCH 15/40] Validate label when writing. --- .../src/Resources/Strings.resx | 3 +++ .../Security/Cryptography/PemEncoding.cs | 24 ++++++++++++----- .../tests/PemEncodingTests.cs | 27 +++++++++++++++++-- 3 files changed, 46 insertions(+), 8 deletions(-) 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 30d98b05d68292..3b48346514c4cd 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography.Encoding/src/Resources/Strings.resx @@ -120,6 +120,9 @@ 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. 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 index 77aa922e10f846..777f825ad99367 100644 --- 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 @@ -349,7 +349,11 @@ public static int GetEncodedSize(int labelLength, int dataLength) /// exceeds the maximum possible encoded data length. /// /// - /// The PEM is too large to possibly encode. + /// 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) { @@ -369,7 +373,11 @@ static void WriteBase64(ReadOnlySpan bytes, Span dest, ref int offse offset += 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) @@ -385,14 +393,14 @@ static void WriteBase64(ReadOnlySpan bytes, Span dest, ref int offse Write(NewLine, destination, ref charsWritten); ReadOnlySpan remainingData = data; - while (remainingData.Length >= 48) + while (remainingData.Length >= BytesPerLine) { - WriteBase64(remainingData[..48], destination, ref charsWritten); - remainingData = remainingData[48..]; + WriteBase64(remainingData[..BytesPerLine], destination, ref charsWritten); + remainingData = remainingData[BytesPerLine..]; Write(NewLine, destination, ref charsWritten); } - Debug.Assert(remainingData.Length < 48); + Debug.Assert(remainingData.Length < BytesPerLine); if (remainingData.Length > 0) { @@ -432,7 +440,11 @@ static void WriteBase64(ReadOnlySpan bytes, Span dest, ref int offse /// exceeds the maximum possible encoded data length. /// /// - /// The PEM is too large to possibly encode. + /// The resulting PEM-encoded text is larger than . + /// + /// - or - + /// + /// contains invalid characters. /// public static char[] Write(ReadOnlySpan label, ReadOnlySpan data) { diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs index aace2dd019a8db..61ef46763fddc6 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -39,14 +39,29 @@ public static void TryFind_True_IncompletePreebPrefixed() public static void TryFind_True_CompletePreebPrefixedDifferentLabel() { string content = "-----BEGIN FAIL----- -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out _)); + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); } [Fact] public static void TryFind_True_CompletePreebPrefixedSameLabel() { string content = "-----BEGIN TEST----- -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out _)); + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal(32..36, fields.Label); + Assert.Equal(42..46, fields.Base64Data); + Assert.Equal(21..65, fields.Location); + Assert.Equal(3, fields.DecodedDataLength); + } + + [Fact] + public static void TryFind_True_PreebEndingOverlap() + { + string content = "-----BEGIN TEST -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + Assert.Equal(27..31, fields.Label); + Assert.Equal(37..41, fields.Base64Data); + Assert.Equal(16..60, fields.Location); + Assert.Equal(3, fields.DecodedDataLength); } [Fact] @@ -561,6 +576,14 @@ public static void TryWrite_EcKey() 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_Empty() { From 9a84a4dd86cad1d83a3170ae381d22cb3756e166 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 20 Feb 2020 14:34:36 -0500 Subject: [PATCH 16/40] Use clearer increment. --- .../src/System/Security/Cryptography/PemEncoding.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index 777f825ad99367..a4b8b2f8c100a8 100644 --- 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 @@ -308,7 +308,10 @@ public static int GetEncodedSize(int labelLength, int dataLength) // dataLength is already known to not overflow here int encodedDataLength = ((dataLength + 2) / 3) << 2; int lineCount = Math.DivRem(encodedDataLength, EncodedLineLength, out int remainder); - lineCount += ((remainder >> 31) - (-remainder >> 31)); //Increment lineCount if remainder is positive. + + if (remainder > 0) + lineCount++; + int encodedDataLengthWithBreaks = encodedDataLength + lineCount; if (int.MaxValue - encodedDataLengthWithBreaks < totalEncapLength) From 3985611ff5a8d054739f54f0552f6a8e88200e15 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 21 Feb 2020 08:52:31 -0500 Subject: [PATCH 17/40] Documentation feedback. --- .../Security/Cryptography/PemEncoding.cs | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) 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 index a4b8b2f8c100a8..a773a2a74aaeec 100644 --- 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 @@ -7,7 +7,9 @@ namespace System.Security.Cryptography { /// - /// RFC-7468 PEM (Privacy-Enhanced Mail) parsing and encoding. + /// 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 { @@ -20,14 +22,14 @@ public static class PemEncoding /// Finds the first PEM-encoded data. /// /// - /// A span containing the PEM encoded data. + /// The text containing the PEM-encoded data. /// /// - /// does not contain a well-formed PEM encoded value. + /// does not contain a well-formed PEM-encoded value. /// /// - /// A structure that contains the location, label, and - /// data location of the encoded data. + /// A value that specifies the location, label, and data location of + /// the encoded data. /// public static PemFields Find(ReadOnlySpan pemData) { @@ -42,14 +44,16 @@ public static PemFields Find(ReadOnlySpan pemData) /// Attempts to find the first PEM-encoded data. /// /// - /// A span containing the PEM encoded data. + /// The text containing the PEM-encoded data. /// /// - /// When this method returns true, the found structure - /// that contains the location, label, and data location of the 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. + /// true if PEM-encoded data was found; otherwise false. /// public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) { @@ -245,8 +249,8 @@ static bool IsBase64Char(char c) => } /// - /// Given the length of a label and binary data, determines the - /// length of an encoded PEM in characters. + /// 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. @@ -273,7 +277,7 @@ static bool IsBase64Char(char c) => /// exceeds the maximum possible encoded data length. /// /// - /// The PEM is too large to encode in a signed 32-bit integer. + /// The length of the PEM-encoded value is larger than . /// public static int GetEncodedSize(int labelLength, int dataLength) { @@ -321,28 +325,30 @@ public static int GetEncodedSize(int labelLength, int dataLength) } /// - /// Tries to write PEM encoded data to . + /// Tries to write the provided data and label as PEM-encoded data into + /// a provided buffer. /// /// - /// The label to encode. + /// The label to write. /// /// - /// The data to encode. + /// The data to write. /// /// - /// The destination to write the PEM encoded data to. + /// The buffer to receive the PEM-encoded text. /// /// - /// When this method returns true, this parameter contains the number of characters - /// written to the buffer. + /// When this method returns, this parameter contains the number of characters + /// written to . This parameter is treated + /// as uninitialized. /// /// - /// true if the buffer is large enough to contain - /// the encoded PEM, otherwise false. + /// 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 RFC-7468. Unix-style line endings are used for line breaks. + /// recommended wrapping of IETF RFC 7468. Unix-style line endings are used for line breaks. /// /// /// exceeds the maximum possible label length. @@ -371,7 +377,10 @@ static void WriteBase64(ReadOnlySpan bytes, Span dest, ref int offse bool success = Convert.TryToBase64Chars(bytes, dest[offset..], out int base64Written); if (!success) + { + Debug.Fail("Convert.TryToBase64Chars failed with a pre-sized buffer"); throw new CryptographicException(); + } offset += base64Written; } From 5c9ebd37988e30c569a60b7b67990bba253a1cd6 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 21 Feb 2020 10:17:00 -0500 Subject: [PATCH 18/40] Formatting cleanup. --- .../Security/Cryptography/PemEncoding.cs | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) 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 index a773a2a74aaeec..178543b4f90cf1 100644 --- 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 @@ -31,6 +31,10 @@ public static class PemEncoding /// 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)) @@ -55,6 +59,10 @@ public static PemFields Find(ReadOnlySpan pemData) /// /// 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 @@ -94,7 +102,6 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) } int labelEndingIndex = labelStartIndex + preebEndIndex; - int contentStartIndex = labelEndingIndex + Ending.Length; ReadOnlySpan label = pemArea[labelStartIndex..labelEndingIndex]; // There could be a preeb that is valid after this one if it has an invalid @@ -104,13 +111,15 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) goto next_after_label; } - Range labelRange = (areaOffset + labelStartIndex)..(areaOffset + labelEndingIndex); + int contentStartIndex = labelEndingIndex + Ending.Length; + Range labelRange = (areaOffset + labelStartIndex).. + (areaOffset + labelEndingIndex); int postebLength = PostEBPrefix.Length + label.Length + Ending.Length; - Span postebBuffer = postebLength > postebStackBufferSize ? new char[postebLength] : postebStackBuffer; - PostEBPrefix.AsSpan().CopyTo(postebBuffer); - label.CopyTo(postebBuffer[PostEBPrefix.Length..]); - Ending.AsSpan().CopyTo(postebBuffer[(PostEBPrefix.Length + label.Length)..]); - ReadOnlySpan posteb = postebBuffer[..postebLength]; + + Span postebBuffer = postebLength > postebStackBufferSize + ? new char[postebLength] + : postebStackBuffer; + ReadOnlySpan posteb = WritePostEB(label, postebBuffer); int postebStartIndex = pemArea[contentStartIndex..].IndexOf(posteb); if (postebStartIndex < 0) @@ -127,7 +136,8 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) goto next_after_label; } - Range contentRange = (areaOffset + contentStartIndex)..(areaOffset + contentEndIndex); + Range contentRange = (areaOffset + contentStartIndex).. + (areaOffset + contentEndIndex); if (!IsValidBase64(pemArea[contentStartIndex..contentEndIndex], out int base64start, @@ -141,16 +151,19 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) } Range pemRange = (areaOffset + preebIndex)..(areaOffset + pemEndIndex); - Range base64range = (contentStartIndex + base64start + areaOffset)..(contentEndIndex + base64end + areaOffset); + Range base64range = (contentStartIndex + base64start + areaOffset).. + (contentEndIndex + base64end + areaOffset); fields = new PemFields(labelRange, base64range, pemRange, decodedSize); return true; next_after_label: - if (labelEndingIndex == 0) + if (labelEndingIndex <= 0) { - // We somehow ended up in a situation where we will advance 0 characters, which means we'll - // we'll probably end up here again in a loop. To avoid getting stuck in a loop, detect this - // situation and return. + // 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. + Debug.Assert(labelEndingIndex > 0); fields = default; return false; } @@ -160,6 +173,16 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) 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[PostEBPrefix.Length..]); + Ending.AsSpan().CopyTo(destination[(PostEBPrefix.Length + label.Length)..]); + return destination[..size]; + } } private static bool IsValidLabel(ReadOnlySpan data) From 8ceff01e99259b056b79e0b2469a6457224ac9f5 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 21 Feb 2020 11:11:01 -0500 Subject: [PATCH 19/40] Remove line handling tests. There is no more line-specific handling as part of moving to a better interpretation of "lax". --- .../tests/PemEncodingTests.cs | 88 ------------------- 1 file changed, 88 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs index 61ef46763fddc6..356e9898e5ae3a 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -87,94 +87,6 @@ public static void TryFind_True_Minimum() Assert.Equal(0, fields.DecodedDataLength); } - [Fact] - public static void TryFind_True_Simple() - { - string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - 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 static void TryFind_True_WindowsStyleEol() - { - string content = "-----BEGIN TEST-----\r\nZm\r\n9v\r\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("TEST", content[fields.Label]); - Assert.Equal(content, content[fields.Location]); - Assert.Equal("Zm\r\n9v", content[fields.Base64Data]); - Assert.Equal(3, fields.DecodedDataLength); - } - - [Fact] - public static void TryFind_True_EolMixed() - { - string content = "-----BEGIN TEST-----\rZm\r\n9v\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("TEST", content[fields.Label]); - Assert.Equal(content, content[fields.Location]); - Assert.Equal("Zm\r\n9v", content[fields.Base64Data]); - Assert.Equal(3, fields.DecodedDataLength); - } - - [Fact] - public static void TryFind_True_OneLine() - { - string content = "-----BEGIN TEST-----Zm9v-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - 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 static void TryFind_True_MultiLineBase64() - { - string content = "-----BEGIN TEST-----\nZm\n9v\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("TEST", content[fields.Label]); - Assert.Equal(content, content[fields.Location]); - Assert.Equal("Zm\n9v", content[fields.Base64Data]); - Assert.Equal(3, fields.DecodedDataLength); - } - - [Fact] - public static void TryFind_True_PrecedingLines() - { - string content = "boop\n-----BEGIN TEST-----\nZm9v\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("TEST", content[fields.Label]); - Assert.Equal(content["boop\n".Length..], content[fields.Location]); - Assert.Equal("Zm9v", content[fields.Base64Data]); - Assert.Equal(3, fields.DecodedDataLength); - } - - [Fact] - public static void TryFind_True_PrecedingLines_WindowsEolStyle() - { - string content = "boop\r\n-----BEGIN TEST-----\r\nZm9v\r\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("TEST", content[fields.Label]); - Assert.Equal(content["boop\r\n".Length..], content[fields.Location]); - Assert.Equal("Zm9v", content[fields.Base64Data]); - Assert.Equal(3, fields.DecodedDataLength); - } - - [Fact] - public static void TryFind_True_PrecedingLines_ReturnLines() - { - string content = "boop\r-----BEGIN TEST-----\rZm9v\r-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("TEST", content[fields.Label]); - Assert.Equal(content["boop\r".Length..], content[fields.Location]); - Assert.Equal("Zm9v", content[fields.Base64Data]); - Assert.Equal(3, fields.DecodedDataLength); - } - [Fact] public static void TryFind_True_PrecedingLinesAndWhitespaceBeforePreeb() { From 43ed5b5053371bc96c7b31ebce63407293bb39b8 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 25 Feb 2020 17:05:59 -0500 Subject: [PATCH 20/40] Some test refactoring. --- .../tests/PemEncodingTests.cs | 187 +++++++++++------- 1 file changed, 117 insertions(+), 70 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs index 356e9898e5ae3a..e2f51d838c7ad5 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -18,10 +18,13 @@ public static void Find_ThrowsWhenNoPem() } [Fact] - public static void Find_Simple() + public static void Find_Success_Simple() { string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST-----"; - PemFields fields = PemEncoding.Find(content); + 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]); @@ -29,116 +32,125 @@ public static void Find_Simple() } [Fact] - public static void TryFind_True_IncompletePreebPrefixed() + public static void Find_Success_IncompletePreebPrefixed() { string content = "-----BEGIN FAIL -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out _)); + AssertPemFound(content, + expectedLocation: 16..60, + expectedBase64: 37..41, + expectedLabel: 27..31); } [Fact] - public static void TryFind_True_CompletePreebPrefixedDifferentLabel() + public static void Find_Success_CompletePreebPrefixedDifferentLabel() { string content = "-----BEGIN FAIL----- -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); + PemFields fields = AssertPemFound(content, + expectedLocation: 21..65, + expectedBase64: 42..46, + expectedLabel: 32..36); + + Assert.Equal("TEST", content[fields.Label]); } [Fact] - public static void TryFind_True_CompletePreebPrefixedSameLabel() + public static void Find_Success_CompletePreebPrefixedSameLabel() { string content = "-----BEGIN TEST----- -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal(32..36, fields.Label); - Assert.Equal(42..46, fields.Base64Data); - Assert.Equal(21..65, fields.Location); - Assert.Equal(3, fields.DecodedDataLength); + PemFields fields = AssertPemFound(content, + expectedLocation: 21..65, + expectedBase64: 42..46, + expectedLabel: 32..36); + + Assert.Equal("TEST", content[fields.Label]); } [Fact] - public static void TryFind_True_PreebEndingOverlap() + public static void Find_Success_PreebEndingOverlap() { string content = "-----BEGIN TEST -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal(27..31, fields.Label); - Assert.Equal(37..41, fields.Base64Data); - Assert.Equal(16..60, fields.Location); + 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 static void Find_LargeLabel() + public static void Find_Success_LargeLabel() { string label = new string('A', 275); string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; - PemFields fields = PemEncoding.Find(content); + PemFields fields = AssertPemFound(content, + expectedLocation: 0..587, + expectedBase64: 292..296, + expectedLabel: 11..286); + Assert.Equal(label, content[fields.Label]); - Assert.Equal(content, content[fields.Location]); - Assert.Equal("Zm9v", content[fields.Base64Data]); - Assert.Equal(3, fields.DecodedDataLength); } [Fact] - public static void TryFind_True_Minimum() + public static void Find_Success_Minimum() { string content = "-----BEGIN ----------END -----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal(string.Empty, content[fields.Label]); - Assert.Equal(content, content[fields.Location]); - Assert.Equal(string.Empty, content[fields.Base64Data]); + PemFields fields = AssertPemFound(content, + expectedLocation: 0..30, + expectedBase64: 16..16, + expectedLabel: 11..11); Assert.Equal(0, fields.DecodedDataLength); } [Fact] - public static void TryFind_True_PrecedingLinesAndWhitespaceBeforePreeb() + public static void Find_Success_PrecedingContentAndWhitespaceBeforePreeb() { - string content = "boop\n -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("TEST", content[fields.Label]); - Assert.Equal(content["boop\n ".Length..], content[fields.Location]); - Assert.Equal("Zm9v", content[fields.Base64Data]); - Assert.Equal(3, fields.DecodedDataLength); + string content = "boop -----BEGIN TEST-----\nZm9v\n-----END TEST-----"; + AssertPemFound(content, + expectedLocation: 7..51, + expectedBase64: 28..32, + expectedLabel: 18..22); } [Fact] - public static void TryFind_True_TrailingWhitespaceAfterPosteb() + public static void Find_Success_TrailingWhitespaceAfterPosteb() { string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST----- "; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("TEST", content[fields.Label]); - Assert.Equal(content[..^" ".Length], content[fields.Location]); - Assert.Equal("Zm9v", content[fields.Base64Data]); - Assert.Equal(3, fields.DecodedDataLength); + AssertPemFound(content, + expectedLocation: 0..44, + expectedBase64: 21..25, + expectedLabel: 11..15); } [Fact] - public static void TryFind_True_EmptyLabel() + public static void Find_Success_EmptyLabel() { string content = "-----BEGIN -----\nZm9v\n-----END -----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal(11..11, fields.Label); - Assert.Equal(content, content[fields.Location]); - Assert.Equal("Zm9v", content[fields.Base64Data]); - Assert.Equal(3, fields.DecodedDataLength); + AssertPemFound(content, + expectedLocation: 0..36, + expectedBase64: 17..21, + expectedLabel: 11..11); } [Fact] - public static void TryFind_True_EmptyContent_OneLine() + public static void Find_Success_EmptyContent_OneLine() { string content = "-----BEGIN EMPTY----------END EMPTY-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("EMPTY", content[fields.Label]); - Assert.Equal(content, content[fields.Location]); - Assert.Equal(21..21, fields.Base64Data); + PemFields fields = AssertPemFound(content, + expectedLocation: 0..40, + expectedBase64: 21..21, + expectedLabel: 11..16); Assert.Equal(0, fields.DecodedDataLength); } [Fact] - public static void TryFind_True_EmptyContent_ManyLinesOfWhitespace() + public static void Find_Success_EmptyContent_ManyLinesOfWhitespace() { string content = "-----BEGIN EMPTY-----\n\t\n\t\n\t \n-----END EMPTY-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("EMPTY", content[fields.Label]); - Assert.Equal(content, content[fields.Location]); - Assert.Equal(30..30, fields.Base64Data); + PemFields fields = AssertPemFound(content, + expectedLocation: 0..49, + expectedBase64: 30..30, + expectedLabel: 11..16); Assert.Equal(0, fields.DecodedDataLength); } @@ -250,52 +262,52 @@ public static void TryFind_False_PostEbBeforePreEb() [InlineData("te\x19st")] [InlineData("te st")] //two spaces [InlineData("te- st")] - public static void TryFind_False_InvalidLabel(string label) + public static void Find_Fail_InvalidLabel(string label) { string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; - Assert.False(PemEncoding.TryFind(content, out _)); + AssertNoPemFound(content); } [Fact] - public static void TryFind_False_InvalidBase64() + public static void Find_Fail_InvalidBase64() { string content = "-----BEGIN TEST-----\n$$$$\n-----END TEST-----"; - Assert.False(PemEncoding.TryFind(content, out _)); + AssertNoPemFound(content); } [Fact] - public static void TryFind_False_PrecedingLinesAndSignificantCharsBeforePreeb() + public static void Find_Fail_PrecedingLinesAndSignificantCharsBeforePreeb() { string content = "boop\nbeep-----BEGIN TEST-----\nZm9v\n-----END TEST-----"; - Assert.False(PemEncoding.TryFind(content, out _)); + AssertNoPemFound(content); } [Fact] - public static void TryFind_False_ContentOnPostEbLine() + public static void Find_Fail_ContentOnPostEbLine() { string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST-----boop"; - Assert.False(PemEncoding.TryFind(content, out _)); + AssertNoPemFound(content); } [Fact] - public static void TryFind_False_MismatchedLabels() + public static void Find_Fail_MismatchedLabels() { string content = "-----BEGIN TEST-----\nZm9v\n-----END FAIL-----"; - Assert.False(PemEncoding.TryFind(content, out _)); + AssertNoPemFound(content); } [Fact] - public static void TryFind_False_NoPostEncapBoundary() + public static void Find_Fail_NoPostEncapBoundary() { string content = "-----BEGIN TEST-----\nZm9v\n"; - Assert.False(PemEncoding.TryFind(content, out _)); + AssertNoPemFound(content); } [Fact] - public static void TryFind_False_IncompletePostEncapBoundary() + public static void Find_Fail_IncompletePostEncapBoundary() { string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST"; - Assert.False(PemEncoding.TryFind(content, out _)); + AssertNoPemFound(content); } [Fact] @@ -521,5 +533,40 @@ public static void Write_Simple() "-----END FANCY DATA-----"; Assert.Equal(expected, result); } + + private static PemFields AssertPemFound( + ReadOnlySpan input, + Range expectedLocation, + Range expectedBase64, + Range expectedLabel) + { + bool tryFind = PemEncoding.TryFind(input, out PemFields tryFields); + Assert.True(tryFind, "TryFind did not succeed but was expected to"); + + PemFields fields = PemEncoding.Find(input); + Assert.Equal(fields.Base64Data, tryFields.Base64Data); + Assert.Equal(fields.Location, tryFields.Location); + Assert.Equal(fields.Label, tryFields.Label); + Assert.Equal(fields.DecodedDataLength, tryFields.DecodedDataLength); + + return fields; + } + + private static void AssertNoPemFound(ReadOnlySpan input) + { + bool tryFind = PemEncoding.TryFind(input, out _); + Assert.False(tryFind, "TryFind did succeed but was not expected to"); + + //Can't use AssertExtensions because it requires capturing a ref struct + try + { + PemEncoding.Find(input); + } + catch (ArgumentException ae) + { + Assert.Equal("pemData", ae.ParamName); + // Pass + } + } } } From a991f1ed0e896cd7062e1e27ed85247a49d7e9d5 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 10 Mar 2020 10:31:32 -0400 Subject: [PATCH 21/40] WIP --- .../Security/Cryptography/PemEncoding.cs | 93 +++++++++++-------- 1 file changed, 53 insertions(+), 40 deletions(-) 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 index 178543b4f90cf1..2be870b1d7fc31 100644 --- 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 @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using System.Runtime.CompilerServices; namespace System.Security.Cryptography { @@ -95,6 +96,8 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) int preebEndIndex = pemArea[labelStartIndex..].IndexOf(Ending); + // There is no ending sequence, -----, in the remainder of + // the document. Therefore, there can never be a complete PreEB. if (preebEndIndex < 0) { fields = default; @@ -139,15 +142,12 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) Range contentRange = (areaOffset + contentStartIndex).. (areaOffset + contentEndIndex); - if (!IsValidBase64(pemArea[contentStartIndex..contentEndIndex], + if (!TryCountBase64(pemArea[contentStartIndex..contentEndIndex], out int base64start, out int base64end, out int decodedSize)) { - Debug.Assert(labelEndingIndex > 0); - areaOffset += labelEndingIndex; - pemArea = pemArea[labelEndingIndex..]; - continue; + goto next_after_label; } Range pemRange = (areaOffset + preebIndex)..(areaOffset + pemEndIndex); @@ -163,7 +163,6 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) // 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. - Debug.Assert(labelEndingIndex > 0); fields = default; return false; } @@ -220,57 +219,71 @@ private static bool IsValidLabel(ReadOnlySpan data) return true; } - private static bool IsValidBase64( - ReadOnlySpan data, + private static bool TryCountBase64( + ReadOnlySpan str, out int base64Start, out int base64End, - out int decodedSize) + out int base64DecodedSize) { - static bool IsBase64Char(char c) => - (c >= '0' && c <= '9') || - (c >= 'A' && c <= 'Z') || - (c >= 'a' && c <= 'z') || - c == '+' || c == '/' || c == '='; - - int base64chars = 0; - int precedingWhiteSpace = 0; - int trailingWhiteSpace = 0; - for (int index = 0; index < data.Length; index++) + base64Start = 0; + base64End = str.Length; + + if (str.IsEmpty) { - char c = data[index]; + base64DecodedSize = 0; + return true; + } - if (IsBase64Char(c)) - { - trailingWhiteSpace = 0; - base64chars++; - } - else if (c == ' ' || c == '\n' || c == '\r' || - c == '\t' || c == '\v') + int significantCharacters = 0; + int paddingCharacters = 0; + + for (int i = 0; i < str.Length; i++) + { + char ch = str[i]; + + // Match whitespace characters from Convert.Base64 + if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') { - if (base64chars == 0) - { - precedingWhiteSpace++; - } + if (significantCharacters == 0) + base64Start++; else - { - trailingWhiteSpace++; - } + base64End--; + + continue; } + + base64End = str.Length; + + if (ch == '=') + paddingCharacters++; + else if (paddingCharacters == 0 && IsBase64Character(ch)) + significantCharacters++; else { - base64Start = default; - base64End = default; - decodedSize = 0; + base64DecodedSize = 0; return false; } } - base64Start = precedingWhiteSpace; - base64End = -trailingWhiteSpace; - decodedSize = (base64chars * 3) / 4; + if (paddingCharacters > 2 || + (paddingCharacters + significantCharacters & 0b11) != 0) + { + base64DecodedSize = 0; + return false; + } + + base64DecodedSize = (int)((significantCharacters * 3L) >> 2); 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; + } + /// /// Determines the length of a PEM-encoded value, in characters, /// given the length of a label and binary data. From 422a8ede1c0489c8e40cb4ab127774b585ec89db Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 10 Mar 2020 15:06:37 -0400 Subject: [PATCH 22/40] Fix tests and base64 location --- .../Security/Cryptography/PemEncoding.cs | 29 +++++++++++-------- .../tests/PemEncodingTests.cs | 6 +++- 2 files changed, 22 insertions(+), 13 deletions(-) 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 index 2be870b1d7fc31..1fbd1347a6e2c1 100644 --- 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 @@ -75,8 +75,8 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) return false; } - const int postebStackBufferSize = 256; - Span postebStackBuffer = stackalloc char[postebStackBufferSize]; + const int PostebStackBufferSize = 256; + Span postebStackBuffer = stackalloc char[PostebStackBufferSize]; int areaOffset = 0; int preebIndex; ReadOnlySpan pemArea = pemData; @@ -85,6 +85,8 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) int labelStartIndex = preebIndex + PreEBPrefix.Length; int preebIndexInFullData = preebIndex + areaOffset; + // If there are any previous characters, the one prior to the PreEB + // must be a white space character. if (preebIndexInFullData > 0 && !char.IsWhiteSpace(pemData[preebIndexInFullData - 1])) { @@ -97,7 +99,8 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) int preebEndIndex = pemArea[labelStartIndex..].IndexOf(Ending); // There is no ending sequence, -----, in the remainder of - // the document. Therefore, there can never be a complete PreEB. + // the document. Therefore, there can never be a complete PreEB + // and we can exit. if (preebEndIndex < 0) { fields = default; @@ -111,7 +114,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) // label, so move from there. if (!IsValidLabel(label)) { - goto next_after_label; + goto NextAfterLabel; } int contentStartIndex = labelEndingIndex + Ending.Length; @@ -119,7 +122,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) (areaOffset + labelEndingIndex); int postebLength = PostEBPrefix.Length + label.Length + Ending.Length; - Span postebBuffer = postebLength > postebStackBufferSize + Span postebBuffer = postebLength > PostebStackBufferSize ? new char[postebLength] : postebStackBuffer; ReadOnlySpan posteb = WritePostEB(label, postebBuffer); @@ -127,16 +130,18 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) if (postebStartIndex < 0) { - goto next_after_label; + goto NextAfterLabel; } int contentEndIndex = postebStartIndex + contentStartIndex; int pemEndIndex = contentEndIndex + postebLength; + // The PostEB must either end at the end of the string, or + // have at least one white space character after it. if (pemEndIndex < pemArea.Length - 1 && !char.IsWhiteSpace(pemArea[pemEndIndex])) { - goto next_after_label; + goto NextAfterLabel; } Range contentRange = (areaOffset + contentStartIndex).. @@ -147,16 +152,16 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) out int base64end, out int decodedSize)) { - goto next_after_label; + goto NextAfterLabel; } Range pemRange = (areaOffset + preebIndex)..(areaOffset + pemEndIndex); Range base64range = (contentStartIndex + base64start + areaOffset).. - (contentEndIndex + base64end + areaOffset); + (contentStartIndex + base64end + areaOffset); fields = new PemFields(labelRange, base64range, pemRange, decodedSize); return true; - next_after_label: + NextAfterLabel: if (labelEndingIndex <= 0) { // We somehow ended up in a situation where we will advance @@ -241,7 +246,7 @@ private static bool TryCountBase64( { char ch = str[i]; - // Match whitespace characters from Convert.Base64 + // Match white space characters from Convert.Base64 if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') { if (significantCharacters == 0) @@ -281,7 +286,7 @@ private static bool IsBase64Character(char ch) { uint c = (uint)ch; return c == '+' || c == '/' || - c - '0' < 10 || c - 'A' < 26 || c - 'a' < 26; + c - '0' < 10 || c - 'A' < 26 || c - 'a' < 26; } /// diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs index e2f51d838c7ad5..eb5cda48276741 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -84,7 +84,7 @@ public static void Find_Success_LargeLabel() string label = new string('A', 275); string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; PemFields fields = AssertPemFound(content, - expectedLocation: 0..587, + expectedLocation: 0..586, expectedBase64: 292..296, expectedLabel: 11..286); @@ -549,6 +549,10 @@ private static PemFields AssertPemFound( Assert.Equal(fields.Label, tryFields.Label); Assert.Equal(fields.DecodedDataLength, tryFields.DecodedDataLength); + Assert.Equal(expectedBase64, tryFields.Base64Data); + Assert.Equal(expectedLocation, tryFields.Location); + Assert.Equal(expectedLabel, tryFields.Label); + return fields; } From 3a222eeaaec287195769b7c55fe303a22484fbad Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 10 Mar 2020 15:39:06 -0400 Subject: [PATCH 23/40] Use an offset helper to reduce IndexOf gymnastics. --- .../Security/Cryptography/PemEncoding.cs | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) 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 index 1fbd1347a6e2c1..84d0b2737bb66f 100644 --- 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 @@ -79,24 +79,19 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) Span postebStackBuffer = stackalloc char[PostebStackBufferSize]; int areaOffset = 0; int preebIndex; - ReadOnlySpan pemArea = pemData; - while ((preebIndex = pemArea.IndexOf(PreEBPrefix)) >= 0) + while ((preebIndex = pemData.IndexOf(PreEBPrefix, areaOffset)) >= 0) { int labelStartIndex = preebIndex + PreEBPrefix.Length; - int preebIndexInFullData = preebIndex + areaOffset; // If there are any previous characters, the one prior to the PreEB // must be a white space character. - if (preebIndexInFullData > 0 && - !char.IsWhiteSpace(pemData[preebIndexInFullData - 1])) + if (preebIndex > 0 && !char.IsWhiteSpace(pemData[preebIndex - 1])) { - Debug.Assert(labelStartIndex > 0); areaOffset += labelStartIndex; - pemArea = pemArea[labelStartIndex..]; continue; } - int preebEndIndex = pemArea[labelStartIndex..].IndexOf(Ending); + int preebEndIndex = pemData.IndexOf(Ending, labelStartIndex); // There is no ending sequence, -----, in the remainder of // the document. Therefore, there can never be a complete PreEB @@ -107,8 +102,8 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) return false; } - int labelEndingIndex = labelStartIndex + preebEndIndex; - ReadOnlySpan label = pemArea[labelStartIndex..labelEndingIndex]; + 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. @@ -117,37 +112,33 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) goto NextAfterLabel; } - int contentStartIndex = labelEndingIndex + Ending.Length; - Range labelRange = (areaOffset + labelStartIndex).. - (areaOffset + labelEndingIndex); + 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 = pemArea[contentStartIndex..].IndexOf(posteb); + int postebStartIndex = pemData.IndexOf(posteb, contentStartIndex); if (postebStartIndex < 0) { goto NextAfterLabel; } - int contentEndIndex = postebStartIndex + contentStartIndex; - int pemEndIndex = contentEndIndex + postebLength; + 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 < pemArea.Length - 1 && - !char.IsWhiteSpace(pemArea[pemEndIndex])) + if (pemEndIndex < pemData.Length - 1 && + !char.IsWhiteSpace(pemData[pemEndIndex])) { goto NextAfterLabel; } - Range contentRange = (areaOffset + contentStartIndex).. - (areaOffset + contentEndIndex); + Range contentRange = contentStartIndex..postebStartIndex; - if (!TryCountBase64(pemArea[contentStartIndex..contentEndIndex], + if (!TryCountBase64(pemData[contentRange], out int base64start, out int base64end, out int decodedSize)) @@ -155,14 +146,14 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) goto NextAfterLabel; } - Range pemRange = (areaOffset + preebIndex)..(areaOffset + pemEndIndex); - Range base64range = (contentStartIndex + base64start + areaOffset).. - (contentStartIndex + base64end + areaOffset); + Range pemRange = preebIndex..pemEndIndex; + Range base64range = (contentStartIndex + base64start).. + (contentStartIndex + base64end); fields = new PemFields(labelRange, base64range, pemRange, decodedSize); return true; NextAfterLabel: - if (labelEndingIndex <= 0) + 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, @@ -171,8 +162,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) fields = default; return false; } - areaOffset += labelEndingIndex; - pemArea = pemArea[labelEndingIndex..]; + areaOffset += preebEndIndex; } fields = default; @@ -189,6 +179,18 @@ static ReadOnlySpan WritePostEB(ReadOnlySpan label, Span desti } } + private static int IndexOf(this ReadOnlySpan str, ReadOnlySpan value, int startPosition) + { + int index = str[startPosition..].IndexOf(value); + + if (index == -1) + { + return -1; + } + + return index + startPosition; + } + private static bool IsValidLabel(ReadOnlySpan data) { static bool IsLabelChar(char c) => (uint)(c - 0x21u) <= 0x5du && c != '-'; From 919ba8a81517be5ff6c94ef46d2265da0fdceff2 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 10 Mar 2020 16:37:25 -0400 Subject: [PATCH 24/40] Rename --- .../System/Security/Cryptography/PemEncoding.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) 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 index 84d0b2737bb66f..8dbcd75e2de6df 100644 --- 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 @@ -79,7 +79,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) Span postebStackBuffer = stackalloc char[PostebStackBufferSize]; int areaOffset = 0; int preebIndex; - while ((preebIndex = pemData.IndexOf(PreEBPrefix, areaOffset)) >= 0) + while ((preebIndex = pemData.IndexOfByOffset(PreEBPrefix, areaOffset)) >= 0) { int labelStartIndex = preebIndex + PreEBPrefix.Length; @@ -91,7 +91,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) continue; } - int preebEndIndex = pemData.IndexOf(Ending, labelStartIndex); + 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 @@ -119,7 +119,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) ? new char[postebLength] : postebStackBuffer; ReadOnlySpan posteb = WritePostEB(label, postebBuffer); - int postebStartIndex = pemData.IndexOf(posteb, contentStartIndex); + int postebStartIndex = pemData.IndexOfByOffset(posteb, contentStartIndex); if (postebStartIndex < 0) { @@ -179,16 +179,10 @@ static ReadOnlySpan WritePostEB(ReadOnlySpan label, Span desti } } - private static int IndexOf(this ReadOnlySpan str, ReadOnlySpan value, int startPosition) + private static int IndexOfByOffset(this ReadOnlySpan str, ReadOnlySpan value, int startPosition) { int index = str[startPosition..].IndexOf(value); - - if (index == -1) - { - return -1; - } - - return index + startPosition; + return index == -1 ? -1 : index + startPosition; } private static bool IsValidLabel(ReadOnlySpan data) From 348f997ffd459a91b22a670bd100593d083ea05f Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 17 Mar 2020 16:11:27 -0400 Subject: [PATCH 25/40] Re-organize tests. --- .../System/AssertExtensions.cs | 15 +- .../Security/Cryptography/PemEncoding.cs | 30 +- .../tests/PemEncodingFindTests.cs | 490 ++++++++++++++++++ .../tests/PemEncodingTests.cs | 451 ++++------------ ...ecurity.Cryptography.Encoding.Tests.csproj | 1 + 5 files changed, 611 insertions(+), 376 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs 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 99f1093e1833e8..05bead80c59b5b 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 Exception AssertThrows(ReadOnlySpan span, AssertThrowsActionReadOnly action) where E : Exception { Exception exception; @@ -413,9 +413,11 @@ public static void AssertThrows(ReadOnlySpan span, AssertThrowsActionRe { throw new ThrowsException(typeof(E), exception); } + + return exception; } - public static void AssertThrows(Span span, AssertThrowsAction action) where E : Exception + public static Exception AssertThrows(Span span, AssertThrowsAction action) where E : Exception { Exception exception; @@ -438,6 +440,15 @@ public static void AssertThrows(Span span, AssertThrowsAction action { throw new ThrowsException(typeof(E), exception); } + + return exception; + } + + public static void Throws(string expectedParamName, ReadOnlySpan span, AssertThrowsActionReadOnly action) + where E : ArgumentException + { + ArgumentException exception = (ArgumentException)AssertThrows(span, action); + Assert.Equal(expectedParamName, exception.ParamName); } } } 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 index 8dbcd75e2de6df..f1ad4774bd53ad 100644 --- 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 @@ -403,13 +403,13 @@ public static int GetEncodedSize(int labelLength, int dataLength) /// public static bool TryWrite(ReadOnlySpan label, ReadOnlySpan data, Span destination, out int charsWritten) { - static void Write(ReadOnlySpan str, Span dest, ref int offset) + static int Write(ReadOnlySpan str, Span dest, int offset) { str.CopyTo(dest[offset..]); - offset += str.Length; + return str.Length; } - static void WriteBase64(ReadOnlySpan bytes, Span dest, ref int offset) + static int WriteBase64(ReadOnlySpan bytes, Span dest, int offset) { bool success = Convert.TryToBase64Chars(bytes, dest[offset..], out int base64Written); @@ -419,7 +419,7 @@ static void WriteBase64(ReadOnlySpan bytes, Span dest, ref int offse throw new CryptographicException(); } - offset += base64Written; + return base64Written; } if (!IsValidLabel(label)) @@ -436,31 +436,31 @@ static void WriteBase64(ReadOnlySpan bytes, Span dest, ref int offse } charsWritten = 0; - Write(PreEBPrefix, destination, ref charsWritten); - Write(label, destination, ref charsWritten); - Write(Ending, destination, ref charsWritten); - Write(NewLine, destination, ref charsWritten); + 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) { - WriteBase64(remainingData[..BytesPerLine], destination, ref charsWritten); + charsWritten += WriteBase64(remainingData[..BytesPerLine], destination, charsWritten); + charsWritten += Write(NewLine, destination, charsWritten); remainingData = remainingData[BytesPerLine..]; - Write(NewLine, destination, ref charsWritten); } Debug.Assert(remainingData.Length < BytesPerLine); if (remainingData.Length > 0) { - WriteBase64(remainingData, destination, ref charsWritten); - Write(NewLine, destination, ref charsWritten); + charsWritten += WriteBase64(remainingData, destination, charsWritten); + charsWritten += Write(NewLine, destination, charsWritten); remainingData = default; } - Write(PostEBPrefix, destination, ref charsWritten); - Write(label, destination, ref charsWritten); - Write(Ending, destination, ref charsWritten); + charsWritten += Write(PostEBPrefix, destination, charsWritten); + charsWritten += Write(label, destination, charsWritten); + charsWritten += Write(Ending, destination, charsWritten); return true; } 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 00000000000000..c213ca59539057 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs @@ -0,0 +1,490 @@ +// 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("H-E-L-L-O ")] + [InlineData("HEL-LO")] + public void TryFind_True_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_LabelCharacterBoundaries() + { + string content = $"-----BEGIN !PANIC~~~-----\nAHHH\n-----END !PANIC~~~-----"; + PemFields fields = AssertPemFound(content, + expectedLocation: 0..54, + expectedBase64: 26..30, + expectedLabel: 11..20); + } + + [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("-OOPS")] + [InlineData("te\x7fst")] + [InlineData("te\x19st")] + [InlineData("te st")] //two spaces + [InlineData("te- st")] + 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); + } + + [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 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 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 void TryWrite_BufferTooSmall() + { + char[] buffer = new char[30]; + Assert.False(PemEncoding.TryWrite(default, default, buffer, out _)); + } + + [Fact] + public 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 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 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 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 void TryWrite_Throws_InvalidLabel() + { + char[] buffer = new char[50]; + AssertExtensions.Throws("label", () => + PemEncoding.TryWrite("\n", default, buffer, out _)); + } + + [Fact] + public void Write_Empty() + { + char[] result = PemEncoding.Write(default, default); + Assert.Equal("-----BEGIN -----\n-----END -----", result); + } + + [Fact] + public void Write_Simple() + { + 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 expected = + "-----BEGIN FANCY DATA-----\n" + + "AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYH\n" + + "-----END FANCY DATA-----"; + Assert.Equal(expected, result); + } + + 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 index eb5cda48276741..ec262a5e6f5664 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingTests.cs @@ -10,306 +10,6 @@ namespace System.Security.Cryptography.Encoding.Tests { public static class PemEncodingTests { - [Fact] - public static void Find_ThrowsWhenNoPem() - { - AssertExtensions.Throws("pemData", - () => PemEncoding.Find(string.Empty)); - } - - [Fact] - public static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static 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 static void Find_Success_EmptyLabel() - { - string content = "-----BEGIN -----\nZm9v\n-----END -----"; - AssertPemFound(content, - expectedLocation: 0..36, - expectedBase64: 17..21, - expectedLabel: 11..11); - } - - [Fact] - public static 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 static 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 static void TryFind_True_CommonLabels(string label) - { - string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal(label, content[fields.Label]); - } - - [Theory] - [InlineData("H E L L O")] - [InlineData("H-E-L-L-O")] - [InlineData("H-E-L-L-O ")] - [InlineData("HEL-LO")] - public static void TryFind_True_LabelsWithHyphenSpace(string label) - { - string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal(label, content[fields.Label]); - } - - [Fact] - public static void TryFind_True_LabelCharacterBoundaries() - { - string content = $"-----BEGIN !PANIC~~~-----\nAHHH\n-----END !PANIC~~~-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("!PANIC~~~", content[fields.Label]); - } - - [Fact] - public static void TryFind_True_MultiPem() - { - string content = @" ------BEGIN EC PARAMETERS----- -BgUrgQQACg== ------END EC PARAMETERS----- ------BEGIN EC PRIVATE KEY----- -MHQCAQEEIIpP2qP/mGWDAojQDNrNfUHwYGNPKeO6VLt+POJeCJ3OoAcGBSuBBAAK -oUQDQgAEeDThNbdvTkptgvfNOpETlKBcWDUKs9IcQ/RaFeBntqt+6J875A79YhmD -D7ofwIDcVqzOJQDhSN54EQ17CFQiwg== ------END EC PRIVATE KEY----- -"; - ReadOnlySpan pem = content; - List labels = new List(); - while (PemEncoding.TryFind(pem, out PemFields fields)) - { - labels.Add(pem[fields.Label].ToString()); - pem = pem[fields.Location.End..]; - } - - Assert.Equal(new string[] { "EC PARAMETERS", "EC PRIVATE KEY" }, labels); - } - - [Fact] - public static void TryFind_True_FindsPemAfterPemWithInvalidBase64() - { - string content = @" ------BEGIN TEST----- -$$$$ ------END TEST----- ------BEGIN TEST2----- -Zm9v ------END TEST2-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("TEST2", content[fields.Label]); - Assert.Equal("Zm9v", content[fields.Base64Data]); - } - - [Fact] - public static void TryFind_True_FindsPemAfterPemWithInvalidLabel() - { - string content = @" ------BEGIN ------ -YmFy ------END ------ ------BEGIN TEST2----- -Zm9v ------END TEST2-----"; - Assert.True(PemEncoding.TryFind(content, out PemFields fields)); - Assert.Equal("TEST2", content[fields.Label]); - Assert.Equal("Zm9v", content[fields.Base64Data]); - } - - [Fact] - public static void TryFind_False_Empty() - { - Assert.False(PemEncoding.TryFind(string.Empty, out _)); - } - - [Fact] - public static void TryFind_False_PostEbBeforePreEb() - { - string content = "-----END TEST-----\n-----BEGIN TEST-----\nZm9v"; - Assert.False(PemEncoding.TryFind(content, out _)); - } - - [Theory] - [InlineData("\tOOPS")] - [InlineData(" OOPS")] - [InlineData("-OOPS")] - [InlineData("te\x7fst")] - [InlineData("te\x19st")] - [InlineData("te st")] //two spaces - [InlineData("te- st")] - public static void Find_Fail_InvalidLabel(string label) - { - string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; - AssertNoPemFound(content); - } - - [Fact] - public static void Find_Fail_InvalidBase64() - { - string content = "-----BEGIN TEST-----\n$$$$\n-----END TEST-----"; - AssertNoPemFound(content); - } - - [Fact] - public static void Find_Fail_PrecedingLinesAndSignificantCharsBeforePreeb() - { - string content = "boop\nbeep-----BEGIN TEST-----\nZm9v\n-----END TEST-----"; - AssertNoPemFound(content); - } - - [Fact] - public static void Find_Fail_ContentOnPostEbLine() - { - string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST-----boop"; - AssertNoPemFound(content); - } - - [Fact] - public static void Find_Fail_MismatchedLabels() - { - string content = "-----BEGIN TEST-----\nZm9v\n-----END FAIL-----"; - AssertNoPemFound(content); - } - - [Fact] - public static void Find_Fail_NoPostEncapBoundary() - { - string content = "-----BEGIN TEST-----\nZm9v\n"; - AssertNoPemFound(content); - } - - [Fact] - public static void Find_Fail_IncompletePostEncapBoundary() - { - string content = "-----BEGIN TEST-----\nZm9v\n-----END TEST"; - AssertNoPemFound(content); - } - [Fact] public static void GetEncodedSize_Empty() { @@ -384,6 +84,16 @@ public static void TryWrite_Simple() 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() { @@ -393,6 +103,14 @@ public static void TryWrite_Empty() 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() { @@ -421,6 +139,26 @@ public static void TryWrite_ExactLineNoPadding() 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() { @@ -467,6 +205,27 @@ public static void TryWrite_WrapPadding() 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() { @@ -501,76 +260,50 @@ public static void TryWrite_EcKey() } [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_Empty() - { - char[] result = PemEncoding.Write(default, default); - Assert.Equal("-----BEGIN -----\n-----END -----", result); - } - - [Fact] - public static void Write_Simple() + public static void Write_EcKey() { 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 + 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 = "FANCY DATA"; + string label = "EC PRIVATE KEY"; 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, result); + "-----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); } - private static PemFields AssertPemFound( - ReadOnlySpan input, - Range expectedLocation, - Range expectedBase64, - Range expectedLabel) + [Fact] + public static void TryWrite_Throws_InvalidLabel() { - bool tryFind = PemEncoding.TryFind(input, out PemFields tryFields); - Assert.True(tryFind, "TryFind did not succeed but was expected to"); - - PemFields fields = PemEncoding.Find(input); - Assert.Equal(fields.Base64Data, tryFields.Base64Data); - Assert.Equal(fields.Location, tryFields.Location); - Assert.Equal(fields.Label, tryFields.Label); - Assert.Equal(fields.DecodedDataLength, tryFields.DecodedDataLength); - - Assert.Equal(expectedBase64, tryFields.Base64Data); - Assert.Equal(expectedLocation, tryFields.Location); - Assert.Equal(expectedLabel, tryFields.Label); - - return fields; + char[] buffer = new char[50]; + AssertExtensions.Throws("label", () => + PemEncoding.TryWrite("\n", default, buffer, out _)); } - private static void AssertNoPemFound(ReadOnlySpan input) + [Fact] + public static void Write_Throws_InvalidLabel() { - bool tryFind = PemEncoding.TryFind(input, out _); - Assert.False(tryFind, "TryFind did succeed but was not expected to"); - - //Can't use AssertExtensions because it requires capturing a ref struct - try - { - PemEncoding.Find(input); - } - catch (ArgumentException ae) - { - Assert.Equal("pemData", ae.ParamName); - // Pass - } + 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 aeda157e8801dd..a414cd5a05f5f8 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 @@ -66,6 +66,7 @@ + CommonTest\System\Security\Cryptography\ByteUtils.cs From 11ad10833c75940aae684c8badf3f2aa6959f224 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 17 Mar 2020 16:16:44 -0400 Subject: [PATCH 26/40] Formatting --- .../src/System/Security/Cryptography/PemEncoding.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index f1ad4774bd53ad..6a72bf48a3c900 100644 --- 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 @@ -215,8 +215,8 @@ private static bool IsValidLabel(ReadOnlySpan data) return false; previousSpaceOrHyphen = true; - } + return true; } From 3150dd584574fab614c64487d24c307842d76099 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 17 Mar 2020 17:47:26 -0400 Subject: [PATCH 27/40] Fixup decoding size calculation to not overflow an int --- .../src/System/Security/Cryptography/PemEncoding.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 index 6a72bf48a3c900..2cdf0858b7bcae 100644 --- 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 @@ -266,14 +266,15 @@ private static bool TryCountBase64( } } - if (paddingCharacters > 2 || - (paddingCharacters + significantCharacters & 0b11) != 0) + int totalChars = paddingCharacters + significantCharacters; + + if (paddingCharacters > 2 || (totalChars & 0b11) != 0) { base64DecodedSize = 0; return false; } - base64DecodedSize = (int)((significantCharacters * 3L) >> 2); + base64DecodedSize = (totalChars >> 2) * 3 - paddingCharacters; return true; } From 3f6738e72b98e5e415cd606e04faaac196637b4f Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 17 Mar 2020 18:04:04 -0400 Subject: [PATCH 28/40] Additional code review feedback. --- .../Security/Cryptography/PemEncoding.cs | 8 +++++-- .../System/Security/Cryptography/PemFields.cs | 22 ++++--------------- 2 files changed, 10 insertions(+), 20 deletions(-) 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 index 2cdf0858b7bcae..b59337d0760032 100644 --- 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 @@ -417,7 +417,7 @@ static int WriteBase64(ReadOnlySpan bytes, Span dest, int offset) if (!success) { Debug.Fail("Convert.TryToBase64Chars failed with a pre-sized buffer"); - throw new CryptographicException(); + throw new ArgumentException(); } return base64Written; @@ -498,12 +498,16 @@ static int WriteBase64(ReadOnlySpan bytes, Span dest, int offset) /// 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)) { - throw new CryptographicException(); + Debug.Fail("TryWrite failed with a pre-sized buffer"); + throw new ArgumentException(); } Debug.Assert(charsWritten == encodedSize); 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 index 1090c104c31078..e4e75a03237cd6 100644 --- 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 @@ -18,37 +18,23 @@ internal PemFields(Range label, Range base64data, Range location, int decodedDat } /// - /// The location of the PEM, including the surrounding ecapsulation boundaries. + /// Gets the location of the PEM-encoded text, including the surrounding encapsulation boundaries. /// - /// - /// A marking the locating inside of the data where - /// the PEM was found. - /// public Range Location { get; } /// - /// The location of the label. + /// Gets the location of the label. /// - /// - /// A marking the locating of the label. - /// public Range Label { get; } /// - /// The location of the base64 data inside of the PEM. + /// Gets the location of the base-64 data inside of the PEM. /// - /// - /// A marking the locating of the base64 data, - /// excluding leading and trailing white space. - /// public Range Base64Data { get; } /// - /// The size of the decoded base64, in bytes. + /// Gets the size of the decoded base-64 data, in bytes. /// - /// - /// When decoded, the size of the base64 data in bytes. - /// public int DecodedDataLength { get; } } } From bf18476e585b810c15e4815f6d8cdc4b7404890b Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 17 Mar 2020 18:35:26 -0400 Subject: [PATCH 29/40] Remove duplicate tests. --- .../tests/PemEncodingFindTests.cs | 161 ------------------ 1 file changed, 161 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs index c213ca59539057..9147a77c7efbb5 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs @@ -282,167 +282,6 @@ public void Find_Fail_IncompletePostEncapBoundary() AssertNoPemFound(content); } - [Fact] - public 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 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 void TryWrite_BufferTooSmall() - { - char[] buffer = new char[30]; - Assert.False(PemEncoding.TryWrite(default, default, buffer, out _)); - } - - [Fact] - public 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 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 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 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 void TryWrite_Throws_InvalidLabel() - { - char[] buffer = new char[50]; - AssertExtensions.Throws("label", () => - PemEncoding.TryWrite("\n", default, buffer, out _)); - } - - [Fact] - public void Write_Empty() - { - char[] result = PemEncoding.Write(default, default); - Assert.Equal("-----BEGIN -----\n-----END -----", result); - } - - [Fact] - public void Write_Simple() - { - 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 expected = - "-----BEGIN FANCY DATA-----\n" + - "AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYH\n" + - "-----END FANCY DATA-----"; - Assert.Equal(expected, result); - } - private PemFields AssertPemFound( ReadOnlySpan input, Range expectedLocation, From 16cf85a2be61dbcc3237466e6b4beae6672cce10 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 17 Mar 2020 18:40:14 -0400 Subject: [PATCH 30/40] Add additional tests for base64 padding. --- .../tests/PemEncodingFindTests.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs index 9147a77c7efbb5..b18461e46a855e 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs @@ -282,6 +282,27 @@ public void Find_Fail_IncompletePostEncapBoundary() 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); + } + private PemFields AssertPemFound( ReadOnlySpan input, Range expectedLocation, From 0ede34787e11bce46482891c8b93ba2582dc84a6 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 18 Mar 2020 10:54:26 -0400 Subject: [PATCH 31/40] Additional tests. --- .../Security/Cryptography/PemEncoding.cs | 1 + .../tests/PemEncodingFindTests.cs | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) 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 index b59337d0760032..1a2323ee9b46c6 100644 --- 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 @@ -42,6 +42,7 @@ public static PemFields Find(ReadOnlySpan pemData) { throw new ArgumentException(SR.Argument_PemEncoding_NoPemFound, nameof(pemData)); } + return fields; } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs index b18461e46a855e..6ac02b537ce01d 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs @@ -182,6 +182,16 @@ public void Find_Success_LabelCharacterBoundaries() expectedLabel: 11..20); } + [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() { @@ -303,6 +313,23 @@ public void Find_Fail_InvalidBase64_MissingPadding() 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, From b78ef7e5e34758b0249048f1b575595a087ea5a1 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 18 Mar 2020 14:28:09 -0400 Subject: [PATCH 32/40] Improve assert extensions. --- .../System/AssertExtensions.cs | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) 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 05bead80c59b5b..d1688c9c860b08 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 Exception AssertThrows(ReadOnlySpan span, AssertThrowsActionReadOnly action) where E : Exception + public static E AssertThrows(ReadOnlySpan span, AssertThrowsActionReadOnly action) where E : Exception { Exception exception; @@ -404,20 +404,18 @@ public static Exception AssertThrows(ReadOnlySpan span, AssertThrowsAct 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); - } - - return exception; } - public static Exception AssertThrows(Span span, AssertThrowsAction action) where E : Exception + public static E AssertThrows(Span span, AssertThrowsAction action) where E : Exception { Exception exception; @@ -431,24 +429,31 @@ public static Exception AssertThrows(Span span, AssertThrowsAction a 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 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 void Throws(string expectedParamName, ReadOnlySpan span, AssertThrowsActionReadOnly action) + public static E Throws(string expectedParamName, Span span, AssertThrowsAction action) where E : ArgumentException { - ArgumentException exception = (ArgumentException)AssertThrows(span, action); + E exception = AssertThrows(span, action); Assert.Equal(expectedParamName, exception.ParamName); + return exception; } } } From 430d2c6366f093dd040493def453077094d5f90d Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 18 Mar 2020 14:37:24 -0400 Subject: [PATCH 33/40] Remove wrapping that is not required --- .../src/System/Security/Cryptography/PemEncoding.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 index 1a2323ee9b46c6..43e49d86e755f9 100644 --- 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 @@ -139,17 +139,13 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) Range contentRange = contentStartIndex..postebStartIndex; - if (!TryCountBase64(pemData[contentRange], - out int base64start, - out int base64end, - out int decodedSize)) + 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); + Range base64range = (contentStartIndex + base64start)..(contentStartIndex + base64end); fields = new PemFields(labelRange, base64range, pemRange, decodedSize); return true; From 159df24db61b376b172033c8686f0a6888a981d3 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 18 Mar 2020 14:49:22 -0400 Subject: [PATCH 34/40] Add comment explaining label composition. --- .../src/System/Security/Cryptography/PemEncoding.cs | 8 ++++++++ 1 file changed, 8 insertions(+) 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 index 43e49d86e755f9..2c59ab2487e705 100644 --- 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 @@ -208,6 +208,14 @@ private static bool IsValidLabel(ReadOnlySpan data) 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 a space or hyphen, then we have two consecutive spaces + // or hyphens which is is invalid. if (!isSpaceOrHyphen || previousSpaceOrHyphen) return false; From 7b9c08f2daf6f77c418193d5bc726321cccbff3e Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 18 Mar 2020 14:54:48 -0400 Subject: [PATCH 35/40] Brace for impact. --- .../src/System/Security/Cryptography/PemEncoding.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 index 2c59ab2487e705..9eebaa17bce1bf 100644 --- 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 @@ -217,7 +217,9 @@ private static bool IsValidLabel(ReadOnlySpan data) // also a space or hyphen, then we have two consecutive spaces // or hyphens which is is invalid. if (!isSpaceOrHyphen || previousSpaceOrHyphen) + { return false; + } previousSpaceOrHyphen = true; } @@ -251,9 +253,13 @@ private static bool TryCountBase64( if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') { if (significantCharacters == 0) + { base64Start++; + } else + { base64End--; + } continue; } @@ -261,9 +267,13 @@ private static bool TryCountBase64( base64End = str.Length; if (ch == '=') + { paddingCharacters++; + } else if (paddingCharacters == 0 && IsBase64Character(ch)) + { significantCharacters++; + } else { base64DecodedSize = 0; From 27e120cbeb7b54b1d2712616d309b1aae06a3ccc Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 18 Mar 2020 15:25:16 -0400 Subject: [PATCH 36/40] Remove open-ended ranges and use Slice. --- .../System/Security/Cryptography/PemEncoding.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 index 9eebaa17bce1bf..96af4a4556c2a5 100644 --- 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 @@ -170,15 +170,15 @@ static ReadOnlySpan WritePostEB(ReadOnlySpan label, Span desti int size = PostEBPrefix.Length + label.Length + Ending.Length; Debug.Assert(destination.Length >= size); PostEBPrefix.AsSpan().CopyTo(destination); - label.CopyTo(destination[PostEBPrefix.Length..]); - Ending.AsSpan().CopyTo(destination[(PostEBPrefix.Length + label.Length)..]); - return destination[..size]; + 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[startPosition..].IndexOf(value); + int index = str.Slice(startPosition).IndexOf(value); return index == -1 ? -1 : index + startPosition; } @@ -421,13 +421,13 @@ public static bool TryWrite(ReadOnlySpan label, ReadOnlySpan data, S { static int Write(ReadOnlySpan str, Span dest, int offset) { - str.CopyTo(dest[offset..]); + str.CopyTo(dest.Slice(offset)); return str.Length; } static int WriteBase64(ReadOnlySpan bytes, Span dest, int offset) { - bool success = Convert.TryToBase64Chars(bytes, dest[offset..], out int base64Written); + bool success = Convert.TryToBase64Chars(bytes, dest.Slice(offset), out int base64Written); if (!success) { @@ -460,9 +460,9 @@ static int WriteBase64(ReadOnlySpan bytes, Span dest, int offset) ReadOnlySpan remainingData = data; while (remainingData.Length >= BytesPerLine) { - charsWritten += WriteBase64(remainingData[..BytesPerLine], destination, charsWritten); + charsWritten += WriteBase64(remainingData.Slice(0, BytesPerLine), destination, charsWritten); charsWritten += Write(NewLine, destination, charsWritten); - remainingData = remainingData[BytesPerLine..]; + remainingData = remainingData.Slice(BytesPerLine); } Debug.Assert(remainingData.Length < BytesPerLine); From f3538ccd503633e0d0fc43caf6d69f861298170a Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 18 Mar 2020 18:50:33 -0400 Subject: [PATCH 37/40] Correct IsValidLabel. Last character must be a labelchar. --- .../Security/Cryptography/PemEncoding.cs | 24 +++++++++---------- .../tests/PemEncodingFindTests.cs | 3 ++- 2 files changed, 14 insertions(+), 13 deletions(-) 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 index 96af4a4556c2a5..f7aaa280f2d352 100644 --- 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 @@ -190,19 +190,16 @@ private static bool IsValidLabel(ReadOnlySpan data) if (data.IsEmpty) return true; - // First character of label must be a labelchar, which is a character - // in 0x21..0x7e (both inclusive), except hyphens. - if (!IsLabelChar(data[0])) - return false; + // The first character must be a labelchar, so initialize to false + bool previousIsLabelChar = false; - bool previousSpaceOrHyphen = false; - for (int index = 1; index < data.Length; index++) + for (int index = 0; index < data.Length; index++) { char c = data[index]; if (IsLabelChar(c)) { - previousSpaceOrHyphen = false; + previousIsLabelChar = true; continue; } @@ -214,17 +211,20 @@ private static bool IsValidLabel(ReadOnlySpan data) // 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 a space or hyphen, then we have two consecutive spaces - // or hyphens which is is invalid. - if (!isSpaceOrHyphen || previousSpaceOrHyphen) + // 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; } - previousSpaceOrHyphen = true; + previousIsLabelChar = false; } - return true; + // 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( diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs index 6ac02b537ce01d..489f20779c1394 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs @@ -163,7 +163,6 @@ public void Find_Success_CommonLabels(string label) [Theory] [InlineData("H E L L O")] [InlineData("H-E-L-L-O")] - [InlineData("H-E-L-L-O ")] [InlineData("HEL-LO")] public void TryFind_True_LabelsWithHyphenSpace(string label) { @@ -244,6 +243,8 @@ public void Find_Fail_PostEbBeforePreEb() [InlineData("te\x19st")] [InlineData("te st")] //two spaces [InlineData("te- st")] + [InlineData("test ")] //last is space, must be labelchar + [InlineData("test-")] //last is hypen, must be labelchar public void Find_Fail_InvalidLabel(string label) { string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; From dff24c1c82926a2c4fdc5a6253dfdb16882a5af9 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 18 Mar 2020 20:51:52 -0400 Subject: [PATCH 38/40] Test additions. --- .../tests/PemEncodingFindTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs index 489f20779c1394..78a53084247d7e 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs @@ -164,6 +164,7 @@ public void Find_Success_CommonLabels(string label) [InlineData("H E L L O")] [InlineData("H-E-L-L-O")] [InlineData("HEL-LO")] + [InlineData("H")] public void TryFind_True_LabelsWithHyphenSpace(string label) { string content = $"-----BEGIN {label}-----\nZm9v\n-----END {label}-----"; @@ -238,13 +239,15 @@ public void Find_Fail_PostEbBeforePreEb() [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 hypen, 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}-----"; From 19f45b227446c477c5d08e855d28ecf131b536f4 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 19 Mar 2020 11:39:06 -0400 Subject: [PATCH 39/40] Fix test names --- .../tests/PemEncodingFindTests.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs index 78a53084247d7e..9529109dbe2000 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs @@ -164,14 +164,23 @@ public void Find_Success_CommonLabels(string label) [InlineData("H E L L O")] [InlineData("H-E-L-L-O")] [InlineData("HEL-LO")] - [InlineData("H")] - public void TryFind_True_LabelsWithHyphenSpace(string label) + 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() { From 6735d329a432c7385416c8ea66ccdfe7452c46e9 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 19 Mar 2020 19:36:40 -0400 Subject: [PATCH 40/40] More specific definition of white space. --- .../Security/Cryptography/PemEncoding.cs | 14 ++++-- .../tests/PemEncodingFindTests.cs | 49 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) 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 index f7aaa280f2d352..7232a72205c9c2 100644 --- 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 @@ -86,7 +86,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) // If there are any previous characters, the one prior to the PreEB // must be a white space character. - if (preebIndex > 0 && !char.IsWhiteSpace(pemData[preebIndex - 1])) + if (preebIndex > 0 && !IsWhiteSpaceCharacter(pemData[preebIndex - 1])) { areaOffset += labelStartIndex; continue; @@ -132,7 +132,7 @@ public static bool TryFind(ReadOnlySpan pemData, out PemFields fields) // 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 && - !char.IsWhiteSpace(pemData[pemEndIndex])) + !IsWhiteSpaceCharacter(pemData[pemEndIndex])) { goto NextAfterLabel; } @@ -249,8 +249,7 @@ private static bool TryCountBase64( { char ch = str[i]; - // Match white space characters from Convert.Base64 - if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') + if (IsWhiteSpaceCharacter(ch)) { if (significantCharacters == 0) { @@ -301,6 +300,13 @@ private static bool IsBase64Character(char ch) 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. diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs index 9529109dbe2000..874b862cd4cf80 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/PemEncodingFindTests.cs @@ -191,6 +191,34 @@ public void Find_Success_LabelCharacterBoundaries() 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() { @@ -277,6 +305,27 @@ public void Find_Fail_PrecedingLinesAndSignificantCharsBeforePreeb() 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() {