From 77ffa69194b7ca0412c8972102a9ac8f8c75f318 Mon Sep 17 00:00:00 2001 From: ycrumeyrolle Date: Tue, 23 Apr 2024 00:32:29 +0200 Subject: [PATCH] Add support for explanatory text on PEM decoding --- src/JsonWebToken/Base64.cs | 145 +++++++++------------ src/JsonWebToken/Cryptography/PemParser.cs | 62 ++++++--- src/JsonWebToken/Cryptography/Pkcs1.cs | 24 ++-- src/JsonWebToken/Cryptography/Pkcs8.cs | 20 ++- src/JsonWebToken/Jwk.cs | 14 +- src/JsonWebToken/JwkKeyOpsValues.cs | 1 - test/JsonWebToken.Tests/Base64Tests.cs | 34 ++++- test/JsonWebToken.Tests/ECJwkTests.cs | 29 +++++ test/JsonWebToken.Tests/JsonWebKeyTests.cs | 31 +++++ test/JsonWebToken.Tests/RsaJwkTests.cs | 38 ++++++ 10 files changed, 260 insertions(+), 138 deletions(-) diff --git a/src/JsonWebToken/Base64.cs b/src/JsonWebToken/Base64.cs index 7a35f423..a5d4d42f 100644 --- a/src/JsonWebToken/Base64.cs +++ b/src/JsonWebToken/Base64.cs @@ -3,7 +3,6 @@ using System; using System.Buffers; -using System.Diagnostics; using System.Runtime.CompilerServices; namespace JsonWebToken @@ -12,12 +11,12 @@ namespace JsonWebToken public static class Base64 { /// Decodes a span of UTF-8 base64-encoded text. - /// This method allocate an array of bytes. Use when possible. - public static byte[] Decode(ReadOnlySpan base64) + /// This method allocate an array of bytes. Use when possible. + public static byte[] Decode(ReadOnlySpan base64, bool stripWhitespace = false) { var dataLength = GetArraySizeRequiredToDecode(base64.Length); var data = new byte[dataLength]; - int length = Decode(base64, data); + int length = Decode(base64, data, stripWhitespace); if (length != dataLength) { data = data.AsSpan(0, length).ToArray(); @@ -29,29 +28,29 @@ public static byte[] Decode(ReadOnlySpan base64) #if NETSTANDARD2_0 /// Decodes a string of UTF-8 base64-encoded text into a span of bytes. /// The number of the bytes written to . - public static int Decode(string base64, Span data) + public static int Decode(string base64, Span data, bool stripWhitespace = false) { if (base64 is null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.base64url); } - return Decode(base64.AsSpan(), data); + return Decode(base64.AsSpan(), data, stripWhitespace); } #endif /// Decodes a span of UTF-8 base64-encoded text into a span of bytes. /// The number of the bytes written to . - public static int Decode(ReadOnlySpan base64, Span data) + public static int Decode(ReadOnlySpan base64, Span data, bool stripWhitespace = false) { byte[]? arrayToReturn = null; var buffer = base64.Length > Constants.MaxStackallocBytes ? (arrayToReturn = ArrayPool.Shared.Rent(base64.Length)) - : stackalloc byte[Constants.MaxStackallocBytes]; + : stackalloc byte[base64.Length]; try { int length = Utf8.GetBytes(base64, buffer); - return Decode(buffer.Slice(0, length), data); + return Decode(buffer.Slice(0, length), data, stripWhitespace); } finally { @@ -64,9 +63,9 @@ public static int Decode(ReadOnlySpan base64, Span data) /// Decodes the span of UTF-8 base64-encoded text into a span of bytes. /// The number of the bytes written to . - public static int Decode(ReadOnlySpan base64, Span data) + public static int Decode(ReadOnlySpan base64, Span data, bool stripWhitespace = false) { - var status = Decode(base64, data, out _, out int bytesWritten); + var status = Decode(base64, data, out _, out int bytesWritten, stripWhitespace); if (status != OperationStatus.Done) { ThrowHelper.ThrowOperationNotDoneException(status); @@ -76,68 +75,55 @@ public static int Decode(ReadOnlySpan base64, Span data) } /// Decodes the span of UTF-8 base64-encoded text into binary data. - public static OperationStatus Decode(ReadOnlySpan base64, Span data, out int bytesConsumed, out int bytesWritten) + public static OperationStatus Decode(ReadOnlySpan base64, Span data, out int bytesConsumed, out int bytesWritten, bool stripWhitespace = false) { - int lastWhitespace = base64.LastIndexOfAny(WhiteSpace); - if (lastWhitespace == -1) + if (stripWhitespace) { - return gfoidl.Base64.Base64.Default.Decode(base64, data, out bytesConsumed, out bytesWritten); - } - else - { - byte[]? utf8ArrayToReturn = null; - Span utf8Data = base64.Length > Constants.MaxStackallocBytes - ? (utf8ArrayToReturn = ArrayPool.Shared.Rent(base64.Length)) - : stackalloc byte[Constants.MaxStackallocBytes]; - try + int lastWhitespace = base64.LastIndexOfAny(WhiteSpace); + if (lastWhitespace != -1) { - int firstWhitespace = base64.IndexOfAny(WhiteSpace); - int length = 0; - Span buffer = utf8Data; - if (firstWhitespace != lastWhitespace) + byte[]? utf8ArrayToReturn = null; + Span utf8Data = base64.Length > Constants.MaxStackallocBytes + ? (utf8ArrayToReturn = ArrayPool.Shared.Rent(base64.Length)) + : stackalloc byte[base64.Length]; + try { - while (firstWhitespace != -1) + int length = 0; + int i = 0; + for (; i <= lastWhitespace; i++) { - base64.Slice(0, firstWhitespace).CopyTo(buffer); - buffer = buffer.Slice(firstWhitespace); - length += firstWhitespace; - - // Skip whitespaces - int i = firstWhitespace; - while (++i < base64.Length && IsWhiteSpace(base64[i])) ; + var current = base64[i]; + if (!IsWhiteSpace(current)) + { + utf8Data[length++] = current; + } + } - base64 = base64.Slice(i); - firstWhitespace = base64.IndexOfAny(WhiteSpace); + for (; i < base64.Length; i++) + { + utf8Data[length++] = base64[i]; } - //// Copy the remaining - base64.CopyTo(buffer); - length += base64.Length; + return gfoidl.Base64.Base64.Default.Decode(utf8Data.Slice(0, length), data, out bytesConsumed, out bytesWritten); } - else + finally { - base64.Slice(0, firstWhitespace).CopyTo(buffer); - base64.Slice(firstWhitespace + 1).CopyTo(buffer.Slice(firstWhitespace)); - length = base64.Length - 1; - } - - return gfoidl.Base64.Base64.Default.Decode(utf8Data.Slice(0, length), data, out bytesConsumed, out bytesWritten); - } - finally - { - if (utf8ArrayToReturn != null) - { - ArrayPool.Shared.Return(utf8ArrayToReturn); + if (utf8ArrayToReturn != null) + { + ArrayPool.Shared.Return(utf8ArrayToReturn); + } } } } + + return gfoidl.Base64.Base64.Default.Decode(base64, data, out bytesConsumed, out bytesWritten); } private static bool IsWhiteSpace(byte c) => c == ' ' || (c >= '\t' && c <= '\r'); private static ReadOnlySpan WhiteSpace - => new byte[] { (byte)' ', (byte)'\t', (byte)'\n', (byte)'\v', (byte)'\f', (byte)'\r' }; + => new byte[] { (byte)' ', (byte)'\t', (byte)'\r', (byte)'\n', (byte)'\v', (byte)'\f' }; /// Encodes a span of UTF-8 text into a span of bytes. /// The number of the bytes written to . @@ -189,7 +175,7 @@ public static byte[] Encode(ReadOnlySpan data) int length = Utf8.GetMaxByteCount(data.Length); var utf8Data = length > Constants.MaxStackallocBytes ? (utf8ArrayToReturn = ArrayPool.Shared.Rent(length)) - : stackalloc byte[Constants.MaxStackallocBytes]; + : stackalloc byte[length]; int written = Utf8.GetBytes(data, utf8Data); return Encode(utf8Data.Slice(0, written)); @@ -240,36 +226,33 @@ internal static unsafe bool IsBase64String(ReadOnlySpan value) static bool IsValidBase64Char(char value) { - bool result = false; - if (value <= byte.MaxValue) + if (value > byte.MaxValue) { - byte byteValue = (byte)value; + return false; + } - // 0-9 - if (byteValue >= (byte)'0' && byteValue <= (byte)'9') - { - result = true; - } - else - { - // a-z or A-Z - byte letter = (byte)(byteValue | 0x20); - if (letter >= (byte)'a' && letter <= (byte)'z') - { - result = true; - } - else - { - // + or / or whitespaces - if (byteValue == (byte)'+' || byteValue == (byte)'/' || IsWhiteSpace(byteValue)) - { - result = true; - } - } - } + byte byteValue = (byte)value; + + // 0-9 + if (byteValue >= (byte)'0' && byteValue <= (byte)'9') + { + return true; + } + + // + or / + if (byteValue == (byte)'+' || byteValue == (byte)'/') + { + return true; + } + + // a-z or A-Z + byteValue |= 0x20; + if (byteValue >= (byte)'a' && byteValue <= (byte)'z') + { + return true; } - return result; + return false; } } } diff --git a/src/JsonWebToken/Cryptography/PemParser.cs b/src/JsonWebToken/Cryptography/PemParser.cs index 3537cd52..37a63064 100644 --- a/src/JsonWebToken/Cryptography/PemParser.cs +++ b/src/JsonWebToken/Cryptography/PemParser.cs @@ -4,6 +4,9 @@ namespace JsonWebToken.Cryptography { internal static class PemParser { + internal static ReadOnlySpan BeginPrefix => new[] { '-', '-', '-', '-', '-', 'B', 'E', 'G', 'I', 'N', ' ' }; + internal static ReadOnlySpan EndSuffix => new[] { '-', '-', '-', '-', '-', 'E', 'N', 'D', ' ' }; + public static AsymmetricJwk Read(string key) { if (key is null) @@ -12,29 +15,46 @@ public static AsymmetricJwk Read(string key) } var data = key.AsSpan().Trim(); - if (data.StartsWith(Pkcs8.PrivateKeyPrefix, StringComparison.Ordinal) && data.EndsWith(Pkcs8.PrivateKeySuffix, StringComparison.Ordinal)) - { - return Pkcs8.ReadPrivateKey(data); - } - else if (data.StartsWith(Pkcs8.PublicKeyPrefix, StringComparison.Ordinal) && data.EndsWith(Pkcs8.PublicKeySuffix, StringComparison.Ordinal)) - { - return Pkcs8.ReadPublicKey(data); - } - if (data.StartsWith(Pkcs1.PrivateRsaKeyPrefix, StringComparison.Ordinal) && data.EndsWith(Pkcs1.PrivatRsaKeySuffix, StringComparison.Ordinal)) - { - return Pkcs1.ReadRsaPrivateKey(data); - } - else if (data.StartsWith(Pkcs1.PublicRsaKeyPrefix, StringComparison.Ordinal) && data.EndsWith(Pkcs1.PublicRsaKeySuffix, StringComparison.Ordinal)) + int startOffset = data.IndexOf(BeginPrefix); + int endOffset = data.IndexOf(EndSuffix); + + if (startOffset != -1 && endOffset != -1) { - return Pkcs1.ReadRsaPublicKey(data); - } + if (data.Slice(startOffset + BeginPrefix.Length, Pkcs8.PrivateKeyPrefix.Length).SequenceEqual(Pkcs8.PrivateKeyPrefix) && + data.Slice(endOffset + EndSuffix.Length, Pkcs8.PrivateKeySuffix.Length).SequenceEqual(Pkcs8.PrivateKeySuffix)) + { + return Pkcs8.ReadPrivateKey(data.Slice(startOffset + BeginPrefix.Length + Pkcs8.PrivateKeyPrefix.Length, endOffset - startOffset - Pkcs8.PrivateKeySuffix.Length - BeginPrefix.Length)); + } + else if (data.Slice(startOffset + BeginPrefix.Length, Pkcs8.PublicKeyPrefix.Length).SequenceEqual(Pkcs8.PublicKeyPrefix) && + data.Slice(endOffset + EndSuffix.Length, Pkcs8.PublicKeySuffix.Length).SequenceEqual(Pkcs8.PublicKeySuffix)) + { + return Pkcs8.ReadPublicKey(data.Slice(startOffset + BeginPrefix.Length + Pkcs8.PublicKeyPrefix.Length, endOffset - startOffset - Pkcs8.PublicKeySuffix.Length - BeginPrefix.Length)); + } + else if (data.Slice(startOffset + BeginPrefix.Length, Pkcs1.PrivateRsaKeyLabel.Length).SequenceEqual(Pkcs1.PrivateRsaKeyLabel) && + data.Slice(endOffset + EndSuffix.Length, Pkcs1.PrivateRsaKeyLabel.Length).SequenceEqual(Pkcs1.PrivateRsaKeyLabel)) + { + return Pkcs1.ReadRsaPrivateKey(data.Slice(startOffset + BeginPrefix.Length + Pkcs1.PrivateRsaKeyLabel.Length, endOffset - startOffset - Pkcs1.PrivateRsaKeyLabel.Length - BeginPrefix.Length)); + } + else if (data.Slice(startOffset + BeginPrefix.Length, Pkcs1.PublicRsaKeyLabel.Length).SequenceEqual(Pkcs1.PublicRsaKeyLabel) && + data.Slice(endOffset + EndSuffix.Length, Pkcs1.PublicRsaKeyLabel.Length).SequenceEqual(Pkcs1.PublicRsaKeyLabel)) + { + return Pkcs1.ReadRsaPublicKey(data.Slice(startOffset + BeginPrefix.Length + Pkcs1.PublicRsaKeyLabel.Length, endOffset - startOffset - Pkcs1.PublicRsaKeyLabel.Length - BeginPrefix.Length)); + } #if SUPPORT_ELLIPTIC_CURVE - if (data.StartsWith(Pkcs1.PrivateECKeyPrefix, StringComparison.Ordinal) && data.EndsWith(Pkcs1.PrivateECKeySuffix, StringComparison.Ordinal)) - { - return Pkcs1.ReadECPrivateKey(data); - } + else if (data.Slice(startOffset + BeginPrefix.Length, Pkcs1.PrivateECKeyLabel.Length).SequenceEqual(Pkcs1.PrivateECKeyLabel) && + data.Slice(endOffset + EndSuffix.Length, Pkcs1.PrivateECKeyLabel.Length).SequenceEqual(Pkcs1.PrivateECKeyLabel)) + { + return Pkcs1.ReadECPrivateKey(data.Slice(startOffset + BeginPrefix.Length + Pkcs1.PrivateECKeyLabel.Length, endOffset - startOffset - Pkcs1.PrivateECKeyLabel.Length - BeginPrefix.Length)); + } #endif - throw new ArgumentException("PEM-encoded key be contained within valid prefix and suffix.", nameof(key)); + else + { + throw new ArgumentException($"PEM-encoded key of type {data.Slice(endOffset + EndSuffix.Length, data.Length - endOffset - EndSuffix.Length - 5).ToString()} is not supported.", nameof(key)); + } + } + + + throw new ArgumentException("PEM-encoded key must be contained within valid prefix and suffix.", nameof(key)); } } -} +} \ No newline at end of file diff --git a/src/JsonWebToken/Cryptography/Pkcs1.cs b/src/JsonWebToken/Cryptography/Pkcs1.cs index 2346384d..2fa1c2c4 100644 --- a/src/JsonWebToken/Cryptography/Pkcs1.cs +++ b/src/JsonWebToken/Cryptography/Pkcs1.cs @@ -5,14 +5,11 @@ namespace JsonWebToken.Cryptography { internal static class Pkcs1 { - public static ReadOnlySpan PublicRsaKeyPrefix => new[] { '-', '-', '-', '-', '-', 'B', 'E', 'G', 'I', 'N', ' ', 'R', 'S', 'A', ' ', 'P', 'U', 'B', 'L', 'I', 'C', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; - public static ReadOnlySpan PublicRsaKeySuffix => new[] { '-', '-', '-', '-', '-', 'E', 'N', 'D', ' ', 'R', 'S', 'A', ' ', 'P', 'U', 'B', 'L', 'I', 'C', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; - public static ReadOnlySpan PrivateRsaKeyPrefix => new[] { '-', '-', '-', '-', '-', 'B', 'E', 'G', 'I', 'N', ' ', 'R', 'S', 'A', ' ', 'P', 'R', 'I', 'V', 'A', 'T', 'E', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; - public static ReadOnlySpan PrivatRsaKeySuffix => new[] { '-', '-', '-', '-', '-', 'E', 'N', 'D', ' ', 'R', 'S', 'A', ' ', 'P', 'R', 'I', 'V', 'A', 'T', 'E', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; + public static ReadOnlySpan PublicRsaKeyLabel => new[] { 'R', 'S', 'A', ' ', 'P', 'U', 'B', 'L', 'I', 'C', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; + public static ReadOnlySpan PrivateRsaKeyLabel => new[] { 'R', 'S', 'A', ' ', 'P', 'R', 'I', 'V', 'A', 'T', 'E', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; #if SUPPORT_ELLIPTIC_CURVE - public static ReadOnlySpan PrivateECKeyPrefix => new[] { '-', '-', '-', '-', '-', 'B', 'E', 'G', 'I', 'N', ' ', 'E', 'C', ' ', 'P', 'R', 'I', 'V', 'A', 'T', 'E', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; - public static ReadOnlySpan PrivateECKeySuffix => new[] { '-', '-', '-', '-', '-', 'E', 'N', 'D', ' ', 'E', 'C', ' ', 'P', 'R', 'I', 'V', 'A', 'T', 'E', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; + public static ReadOnlySpan PrivateECKeyLabel => new[] { 'E', 'C', ' ', 'P', 'R', 'I', 'V', 'A', 'T', 'E', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; #endif // SEQUENCE @@ -20,12 +17,11 @@ internal static class Pkcs1 // INTEGER E public static RsaJwk ReadRsaPublicKey(ReadOnlySpan key) { - var data = key.Slice(PublicRsaKeyPrefix.Length, key.Length - PublicRsaKeyPrefix.Length - PublicRsaKeySuffix.Length); byte[] tmpArray; - Span keyData = tmpArray = ArrayPool.Shared.Rent(Base64.GetArraySizeRequiredToDecode(data.Length)); + Span keyData = tmpArray = ArrayPool.Shared.Rent(Base64.GetArraySizeRequiredToDecode(key.Length)); try { - int length = Base64.Decode(data, keyData); + int length = Base64.Decode(key, keyData, stripWhitespace: true); var reader = new AsnReader(keyData.Slice(0, length)); reader = reader.ReadSequence(); @@ -58,12 +54,11 @@ public static RsaJwk ReadRsaPublicKey(ReadOnlySpan key) // INTEGER QI public static RsaJwk ReadRsaPrivateKey(ReadOnlySpan key) { - var data = key.Slice(PrivateRsaKeyPrefix.Length, key.Length - PrivateRsaKeyPrefix.Length - PrivatRsaKeySuffix.Length); byte[] tmpArray; - Span keyData = tmpArray = ArrayPool.Shared.Rent(Base64.GetArraySizeRequiredToDecode(data.Length)); + Span keyData = tmpArray = ArrayPool.Shared.Rent(Base64.GetArraySizeRequiredToDecode(key.Length)); try { - int length = Base64.Decode(data, keyData); + int length = Base64.Decode(key, keyData, stripWhitespace: true); var reader = new AsnReader(keyData.Slice(0, length)); reader = reader.ReadSequence(); var version = reader.ReadInteger(); @@ -111,12 +106,11 @@ public static RsaJwk ReadRsaPrivateKey(ReadOnlySpan key) // BIT STRING public key public static ECJwk ReadECPrivateKey(ReadOnlySpan key) { - var data = key.Slice(PrivateECKeyPrefix.Length, key.Length - PrivateECKeyPrefix.Length - PrivateECKeySuffix.Length); byte[] tmpArray; - Span keyData = tmpArray = ArrayPool.Shared.Rent(Base64.GetArraySizeRequiredToDecode(data.Length)); + Span keyData = tmpArray = ArrayPool.Shared.Rent(Base64.GetArraySizeRequiredToDecode(key.Length)); try { - int length = Base64.Decode(data, keyData); + int length = Base64.Decode(key, keyData, stripWhitespace: true); var reader = new AsnReader(keyData.Slice(0, length)); reader = reader.ReadSequence(); diff --git a/src/JsonWebToken/Cryptography/Pkcs8.cs b/src/JsonWebToken/Cryptography/Pkcs8.cs index fc8dcade..8e9d706d 100644 --- a/src/JsonWebToken/Cryptography/Pkcs8.cs +++ b/src/JsonWebToken/Cryptography/Pkcs8.cs @@ -5,19 +5,18 @@ namespace JsonWebToken.Cryptography { internal static class Pkcs8 { - internal static ReadOnlySpan PublicKeyPrefix => new[] { '-', '-', '-', '-', '-', 'B', 'E', 'G', 'I', 'N', ' ', 'P', 'U', 'B', 'L', 'I', 'C', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; - internal static ReadOnlySpan PublicKeySuffix => new[] { '-', '-', '-', '-', '-', 'E', 'N', 'D', ' ', 'P', 'U', 'B', 'L', 'I', 'C', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; - internal static ReadOnlySpan PrivateKeyPrefix => new[] { '-', '-', '-', '-', '-', 'B', 'E', 'G', 'I', 'N', ' ', 'P', 'R', 'I', 'V', 'A', 'T', 'E', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; - internal static ReadOnlySpan PrivateKeySuffix => new[] { '-', '-', '-', '-', '-', 'E', 'N', 'D', ' ', 'P', 'R', 'I', 'V', 'A', 'T', 'E', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; + internal static ReadOnlySpan PublicKeyPrefix => new[] { 'P', 'U', 'B', 'L', 'I', 'C', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; + internal static ReadOnlySpan PublicKeySuffix => new[] { 'P', 'U', 'B', 'L', 'I', 'C', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; + internal static ReadOnlySpan PrivateKeyPrefix => new[] { 'P', 'R', 'I', 'V', 'A', 'T', 'E', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; + internal static ReadOnlySpan PrivateKeySuffix => new[] { 'P', 'R', 'I', 'V', 'A', 'T', 'E', ' ', 'K', 'E', 'Y', '-', '-', '-', '-', '-' }; public static AsymmetricJwk ReadPublicKey(ReadOnlySpan key) { - var data = key.Slice(PublicKeyPrefix.Length, key.Length - PublicKeyPrefix.Length - PublicKeySuffix.Length); byte[] tmpArray; - Span keyData = tmpArray = ArrayPool.Shared.Rent(Base64.GetArraySizeRequiredToDecode(data.Length)); + Span keyData = tmpArray = ArrayPool.Shared.Rent(Base64.GetArraySizeRequiredToDecode(key.Length)); try { - int length = Base64.Decode(data, keyData); + int length = Base64.Decode(key, keyData, stripWhitespace: true); var reader = new AsnReader(keyData.Slice(0, length)); reader = reader.ReadSequence(); var readerOid = reader.ReadSequence(); @@ -48,12 +47,11 @@ public static AsymmetricJwk ReadPublicKey(ReadOnlySpan key) // NULL or OBJECT IDENTIFIER (EC curve OID) public static AsymmetricJwk ReadPrivateKey(ReadOnlySpan key) { - var data = key.Slice(PrivateKeyPrefix.Length, key.Length - PrivateKeyPrefix.Length - PrivateKeySuffix.Length); byte[] tmpArray; - Span keyData = tmpArray = ArrayPool.Shared.Rent(Base64.GetArraySizeRequiredToDecode(data.Length)); + Span keyData = tmpArray = ArrayPool.Shared.Rent(Base64.GetArraySizeRequiredToDecode(key.Length)); try { - int length = Base64.Decode(data, keyData); + int length = Base64.Decode(key, keyData, stripWhitespace: true); var reader = new AsnReader(keyData.Slice(0, length)); reader = reader.ReadSequence(); reader.ReadInteger(); @@ -346,4 +344,4 @@ public static bool IsRsaKeyOid(int[] oid) oid[6] == 1; } } -} +} \ No newline at end of file diff --git a/src/JsonWebToken/Jwk.cs b/src/JsonWebToken/Jwk.cs index 173b44d2..76115567 100644 --- a/src/JsonWebToken/Jwk.cs +++ b/src/JsonWebToken/Jwk.cs @@ -905,7 +905,7 @@ public static void Validate(string json) keyLength = Base64Url.GetArraySizeRequiredToDecode(n.GetString()!.Length) * 8; } - if (keyLength % 256 != 0 || keyLength < 512) + if ((keyLength & 255) != 0 || keyLength < 512) { throw new JwkValidationException(@$"Invalid key length. Must be a multiple of 256 bits, and at least 512 bits. Current key length is {keyLength}."); } @@ -913,11 +913,12 @@ public static void Validate(string json) CheckRequiredBase64UrlMember(document, JwkParameterNames.E); int privateRsaMembers = CheckOptionalBase64UrlMember(document, JwkParameterNames.D, keyLength); - privateRsaMembers |= CheckOptionalBase64UrlMember(document, JwkParameterNames.P, keyLength / 2) << 1; - privateRsaMembers |= CheckOptionalBase64UrlMember(document, JwkParameterNames.Q, keyLength / 2) << 2; - privateRsaMembers |= CheckOptionalBase64UrlMember(document, JwkParameterNames.DP, keyLength / 2) << 3; - privateRsaMembers |= CheckOptionalBase64UrlMember(document, JwkParameterNames.DQ, keyLength / 2) << 4; - privateRsaMembers |= CheckOptionalBase64UrlMember(document, JwkParameterNames.QI, keyLength / 2) << 5; + int halfKeyLength = keyLength / 2; + privateRsaMembers |= CheckOptionalBase64UrlMember(document, JwkParameterNames.P, halfKeyLength) << 1; + privateRsaMembers |= CheckOptionalBase64UrlMember(document, JwkParameterNames.Q, halfKeyLength) << 2; + privateRsaMembers |= CheckOptionalBase64UrlMember(document, JwkParameterNames.DP, halfKeyLength) << 3; + privateRsaMembers |= CheckOptionalBase64UrlMember(document, JwkParameterNames.DQ, halfKeyLength) << 4; + privateRsaMembers |= CheckOptionalBase64UrlMember(document, JwkParameterNames.QI, halfKeyLength) << 5; if (privateRsaMembers != 0 && privateRsaMembers != (1 | 2 | 4 | 8 | 16 | 32)) { if ((privateRsaMembers & 1) == 0) @@ -1157,7 +1158,6 @@ static bool TryCheckRequiredBase64UrlMember(JsonDocument document, JsonEncodedTe return false; } - static int CheckOptionalBase64UrlMember(JsonDocument document, JsonEncodedText memberName, int length) { if (document.RootElement.TryGetProperty(memberName.EncodedUtf8Bytes, out var value)) diff --git a/src/JsonWebToken/JwkKeyOpsValues.cs b/src/JsonWebToken/JwkKeyOpsValues.cs index cbca02b5..54cb23a8 100644 --- a/src/JsonWebToken/JwkKeyOpsValues.cs +++ b/src/JsonWebToken/JwkKeyOpsValues.cs @@ -10,7 +10,6 @@ namespace JsonWebToken /// http://tools.ietf.org/html/rfc7517#section-4 /// public static class JwkKeyOpsValues - { /// Gets the 'sign' (compute digital signature or MAC) value for the 'key_ops' parameter. public static readonly JsonEncodedText Sign = JsonEncodedText.Encode("sign"); diff --git a/test/JsonWebToken.Tests/Base64Tests.cs b/test/JsonWebToken.Tests/Base64Tests.cs index 012c96f3..22362917 100644 --- a/test/JsonWebToken.Tests/Base64Tests.cs +++ b/test/JsonWebToken.Tests/Base64Tests.cs @@ -6,6 +6,19 @@ namespace JsonWebToken.Tests { public class Base64Tests { + [Theory] + [InlineData("", "")] + [InlineData("SGVsbG8=", "Hello")] + [InlineData("SGVsbG8gV29ybGQ=", "Hello World")] + [InlineData("SGV+bG8=", "He~lo")] + [InlineData("SGV/bG8=", "He\u007flo")] + public void Decode_Valid(string value, string expected) + { + var result = Base64.Decode(Encoding.UTF8.GetBytes(value)); + Assert.NotNull(result); + Assert.Equal(Encoding.UTF8.GetBytes(expected), result); + } + [Theory] [InlineData("", "")] [InlineData("SGVsbG8=", "Hello")] @@ -23,9 +36,9 @@ public class Base64Tests [InlineData(" S G V s b G8gV29ybGQ= ", "Hello World")] [InlineData("SGV+bG8=", "He~lo")] [InlineData("SGV/bG8=", "He\u007flo")] - public void Decode_Valid(string value, string expected) + public void Decode_WithWithspaceStripping_Valid(string value, string expected) { - var result = Base64.Decode(Encoding.UTF8.GetBytes(value)); + var result = Base64.Decode(Encoding.UTF8.GetBytes(value), true); Assert.NotNull(result); Assert.Equal(Encoding.UTF8.GetBytes(expected), result); } @@ -40,5 +53,22 @@ public void Decode_Invalid(string value) { Assert.Throws(() => Base64.Decode(Encoding.UTF8.GetBytes(value))); } + + [Theory] + [InlineData("SGVsbG8\tgV29ybGQ=")] + [InlineData("SGVsbG8\rgV29ybGQ=")] + [InlineData("SGVsbG8\ngV29ybGQ=")] + [InlineData("SGVsbG8\vgV29ybGQ=")] + [InlineData("SGVsbG8\fgV29ybGQ=")] + [InlineData("SGVsbG8 gV29ybGQ=")] + [InlineData(" SGVsbG8gV29ybGQ=")] + [InlineData("SG Vsb G8gV29ybGQ=")] + [InlineData("S G V s b G 8 g V 2 9 y b G Q =")] + [InlineData("S G V s b G8gV29ybGQ=")] + [InlineData(" S G V s b G8gV29ybGQ= ")] + public void Decode_WithoutWithstripping_Invalid(string value) + { + Assert.Throws(() => Base64.Decode(Encoding.UTF8.GetBytes(value), false)); + } } } diff --git a/test/JsonWebToken.Tests/ECJwkTests.cs b/test/JsonWebToken.Tests/ECJwkTests.cs index 32b50512..e419f2ec 100644 --- a/test/JsonWebToken.Tests/ECJwkTests.cs +++ b/test/JsonWebToken.Tests/ECJwkTests.cs @@ -356,10 +356,38 @@ public override void WriteTo() MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgQHs5HRkpurXDPaabivT2IaRoyYt Isuk92Ner/JmgKjYoSumHVmSNfZ9nLTVjxeD08pD548KWrqmJAeZNsDDqQ== -----END PUBLIC KEY-----"; + private const string Pkcs8PemECPrivateKeyExplanatoryText = @" +Subject: CN=Atlantis +Issuer: CN=Atlantis +Validity: from 7/9/2012 3:10:38 AM UTC to 7/9/2013 3:10:37 AM UTC +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgcKEsLbFoRe1W/2jP +whpHKz8E19aFG/Y0ny19WzRSs4qhRANCAASBAezkdGSm6tcM9ppuK9PYhpGjJi0i +y6T3Y16v8maAqNihK6YdWZI19n2ctNWPF4PTykPnjwpauqYkB5k2wMOp +-----END PRIVATE KEY-----this was a key."; + private const string Pkcs1PemECPrivateKeyExplanatoryText = @" +Subject: CN=Atlantis +Issuer: CN=Atlantis +Validity: from 7/9/2012 3:10:38 AM UTC to 7/9/2013 3:10:37 AM UTC +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIHChLC2xaEXtVv9oz8IaRys/BNfWhRv2NJ8tfVs0UrOKoAoGCCqGSM49 +AwEHoUQDQgAEgQHs5HRkpurXDPaabivT2IaRoyYtIsuk92Ner/JmgKjYoSumHVmS +NfZ9nLTVjxeD08pD548KWrqmJAeZNsDDqQ== +-----END EC PRIVATE KEY-----this was a key."; + private const string Pkcs8PemECPublicKeyExplanatoryText = @" +Subject: CN=Atlantis +Issuer: CN=Atlantis +Validity: from 7/9/2012 3:10:38 AM UTC to 7/9/2013 3:10:37 AM UTC +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgQHs5HRkpurXDPaabivT2IaRoyYt +Isuk92Ner/JmgKjYoSumHVmSNfZ9nLTVjxeD08pD548KWrqmJAeZNsDDqQ== +-----END PUBLIC KEY-----this was a key."; [Theory] [InlineData(Pkcs1PemECPrivateKey)] [InlineData(Pkcs8PemECPrivateKey)] + [InlineData(Pkcs1PemECPrivateKeyExplanatoryText)] + [InlineData(Pkcs8PemECPrivateKeyExplanatoryText)] public void FromPem_PrivateKey(string pem) { var key = ECJwk.FromPem(pem); @@ -371,6 +399,7 @@ public void FromPem_PrivateKey(string pem) [Theory] [InlineData(Pkcs8PemECPublicKey)] + [InlineData(Pkcs8PemECPublicKeyExplanatoryText)] public void FromPem_PublicKey(string pem) { var key = ECJwk.FromPem(pem); diff --git a/test/JsonWebToken.Tests/JsonWebKeyTests.cs b/test/JsonWebToken.Tests/JsonWebKeyTests.cs index 92f32f4b..bb181525 100644 --- a/test/JsonWebToken.Tests/JsonWebKeyTests.cs +++ b/test/JsonWebToken.Tests/JsonWebKeyTests.cs @@ -156,6 +156,37 @@ public void CreateFromCertificate_Error() Assert.Null(key); } + [Theory] + [InlineData(@"-----BEGIN PRIVATE KEY----- +MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtz9Z9e6L1V4kt/8C +mtFqhUPJbSU+VDGbk1MsQcPBR3uJ2y0vM9e5qHRYSOBqjmg7UERRHhvKNiUn4Xz0 +KzgGFQIDAQABAkEAr+byNi+cr17FpJH4MCEiPXaKnmkH4c4U52EJtL9yg2gijBrp +Ykat3c2nWb0EGGi5aWgXxQHoi7z97/ACD4X3KQIhAPNyex6GdiBVlNPHOgInTU8a +mARKKVHIXM0SxvxXrRl7AiEAwLI66OpSqftDTv1KUfNe6+hyoh23ggzUSYiWuVT0 +Ya8CHwiO/cUU9RIt8A2B84gf2ZfuV2nPMaSuZpTPFC/K5UsCIQCsJMzx1JuilQAN +acPiMCuFTnRSFYAhozpmsqoLyTREqwIhAMLJlZTGjEB2N+sEazH5ToEczQzKqp7t +9juGNbOPhoEL +-----END PRIVATE KEY-----", 512, true)] + public void CreateFromPem(string pem, int keySize, bool hasPrivateKey) + { + var jwk = Jwk.FromPem(pem); + + Assert.Equal(keySize, jwk.KeySizeInBits); + Assert.Equal(hasPrivateKey, jwk.HasPrivateKey); + } + + [Theory] + [InlineData(@"-----BEGIN CMS----- +MIGDBgsqhkiG9w0BCRABCaB0MHICAQAwDQYLKoZIhvcNAQkQAwgwXgYJKoZIhvcN +AQcBoFEET3icc87PK0nNK9ENqSxItVIoSa0o0S/ISczMs1ZIzkgsKk4tsQ0N1nUM +dvb05OXi5XLPLEtViMwvLVLwSE0sKlFIVHAqSk3MBkkBAJv0Fx0= +-----END CMS-----", "CMS")] + public void CreateFromPem_Invalid_ThrowsException(string pem, string unkwnowLabel) + { + var exception = Assert.Throws(() => Jwk.FromPem(pem)); + Assert.Contains(unkwnowLabel, exception.Message); + } + public static IEnumerable GetJsonKeys() { var fixture = new KeyFixture(); diff --git a/test/JsonWebToken.Tests/RsaJwkTests.cs b/test/JsonWebToken.Tests/RsaJwkTests.cs index 0b6726cc..45573ec1 100644 --- a/test/JsonWebToken.Tests/RsaJwkTests.cs +++ b/test/JsonWebToken.Tests/RsaJwkTests.cs @@ -397,9 +397,45 @@ public override void FromJson_WithProperties(string json) 4GqOaDtQRFEeG8o2JSfhfPQrOAYVAgMBAAE= -----END RSA PUBLIC KEY-----"; + private const string Pkcs8PemRsaPrivateKeyExplanatoryText = @" +This is a key-----BEGIN PRIVATE KEY----- +MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtz9Z9e6L1V4kt/8C +mtFqhUPJbSU+VDGbk1MsQcPBR3uJ2y0vM9e5qHRYSOBqjmg7UERRHhvKNiUn4Xz0 +KzgGFQIDAQABAkEAr+byNi+cr17FpJH4MCEiPXaKnmkH4c4U52EJtL9yg2gijBrp +Ykat3c2nWb0EGGi5aWgXxQHoi7z97/ACD4X3KQIhAPNyex6GdiBVlNPHOgInTU8a +mARKKVHIXM0SxvxXrRl7AiEAwLI66OpSqftDTv1KUfNe6+hyoh23ggzUSYiWuVT0 +Ya8CHwiO/cUU9RIt8A2B84gf2ZfuV2nPMaSuZpTPFC/K5UsCIQCsJMzx1JuilQAN +acPiMCuFTnRSFYAhozpmsqoLyTREqwIhAMLJlZTGjEB2N+sEazH5ToEczQzKqp7t +9juGNbOPhoEL +-----END PRIVATE KEY-----this was a key."; + + private const string Pkcs1PemRsaPrivateKeyExplanatoryText = @" +This is a key-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBALc/WfXui9VeJLf/AprRaoVDyW0lPlQxm5NTLEHDwUd7idstLzPX +uah0WEjgao5oO1BEUR4byjYlJ+F89Cs4BhUCAwEAAQJBAK/m8jYvnK9exaSR+DAh +Ij12ip5pB+HOFOdhCbS/coNoIowa6WJGrd3Np1m9BBhouWloF8UB6Iu8/e/wAg+F +9ykCIQDzcnsehnYgVZTTxzoCJ01PGpgESilRyFzNEsb8V60ZewIhAMCyOujqUqn7 +Q079SlHzXuvocqIdt4IM1EmIlrlU9GGvAh8Ijv3FFPUSLfANgfOIH9mX7ldpzzGk +rmaUzxQvyuVLAiEArCTM8dSbopUADWnD4jArhU50UhWAIaM6ZrKqC8k0RKsCIQDC +yZWUxoxAdjfrBGsx+U6BHM0Myqqe7fY7hjWzj4aBCw== +-----END RSA PRIVATE KEY-----this was a key."; + + private const string Pkcs8PemRsaPublicKeyExplanatoryText = @" +This is a key-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALc/WfXui9VeJLf/AprRaoVDyW0lPlQx +m5NTLEHDwUd7idstLzPXuah0WEjgao5oO1BEUR4byjYlJ+F89Cs4BhUCAwEAAQ== +-----END PUBLIC KEY-----this was a key."; + + private const string Pkcs1PemRsaPublicKeyExplanatoryText = @" +This is a key-----BEGIN RSA PUBLIC KEY----- +MEgCQQC3P1n17ovVXiS3/wKa0WqFQ8ltJT5UMZuTUyxBw8FHe4nbLS8z17modFhI +4GqOaDtQRFEeG8o2JSfhfPQrOAYVAgMBAAE= +-----END RSA PUBLIC KEY-----this was a key."; [Theory] [InlineData(Pkcs1PemRsaPrivateKey)] [InlineData(Pkcs8PemRsaPrivateKey)] + [InlineData(Pkcs1PemRsaPrivateKeyExplanatoryText)] + [InlineData(Pkcs8PemRsaPrivateKeyExplanatoryText)] public void FromPem_PrivateKey(string pem) { var key = RsaJwk.FromPem(pem); @@ -410,6 +446,8 @@ public void FromPem_PrivateKey(string pem) [Theory] [InlineData(Pkcs1PemRsaPublicKey)] [InlineData(Pkcs8PemRsaPublicKey)] + [InlineData(Pkcs1PemRsaPublicKeyExplanatoryText)] + [InlineData(Pkcs8PemRsaPublicKeyExplanatoryText)] public void FromPem_PublicKey(string pem) { var key = RsaJwk.FromPem(pem);