diff --git a/src/Open.IdentityServer/src/Configuration/CryptoHelper.cs b/src/Open.IdentityServer/src/Configuration/CryptoHelper.cs index dee7b712d..f3e4c45be 100644 --- a/src/Open.IdentityServer/src/Configuration/CryptoHelper.cs +++ b/src/Open.IdentityServer/src/Configuration/CryptoHelper.cs @@ -97,6 +97,43 @@ public static HashAlgorithm GetHashAlgorithmForSigningAlgorithm(string signingAl }; } + /// + /// Returns if the algorithm is an RSA algorithm (RSxxx or PSxxx) + /// + /// The algorithm to check. + /// if the algorithm is an RSA algorithm; otherwise, . + public static bool IsRsaAlgorithm(this string algorithm) + { + return algorithm.StartsWith('R') || algorithm.StartsWith('P'); + } + + /// + /// Returns if the algorithm is an EC algorithm (Exxx) + /// + /// The algorithm to check. + /// if the algorithm is an EC algorithm; otherwise, . + public static bool IsEcAlgorithm(this string algorithm) + { + return algorithm.StartsWith('E'); + } + + /// + /// Returns the matching named curve for a given algorithm + /// + /// The algorithm to get the curve name for. + /// The name of the curve corresponding to the algorithm. + /// + public static string GetCurveNameForAlgorithm(this string algorithm) + { + return algorithm switch + { + "ES256" => "P-256", + "ES384" => "P-384", + "ES521" => "P-521", + _ => throw new ArgumentOutOfRangeException(nameof(algorithm), "Unexpected algorithm value for EC Curve") + }; + } + /// /// Returns the matching named curve for RFC 7518 crv value /// diff --git a/src/Open.IdentityServer/src/DataProtection/DataProtectedIdentityServerKeyMaterialConverter.cs b/src/Open.IdentityServer/src/DataProtection/DataProtectedIdentityServerKeyMaterialConverter.cs index 44e2950c1..d586a93cd 100644 --- a/src/Open.IdentityServer/src/DataProtection/DataProtectedIdentityServerKeyMaterialConverter.cs +++ b/src/Open.IdentityServer/src/DataProtection/DataProtectedIdentityServerKeyMaterialConverter.cs @@ -45,8 +45,7 @@ public SigningKey Convert(IdentityServerKeyMaterial keyMaterial) dataProtector.Unprotect(keyMaterial.Data) : keyMaterial.Data; - if (!keyMaterial.IsX509Certificate && - (keyMaterial.Algorithm.StartsWith('R') || keyMaterial.Algorithm.StartsWith('P'))) + if (!keyMaterial.IsX509Certificate && keyMaterial.Algorithm.IsRsaAlgorithm()) { var keyData = JsonSerializer.Deserialize(unprotectedData, Settings); @@ -54,18 +53,12 @@ public SigningKey Convert(IdentityServerKeyMaterial keyMaterial) signingKey.Credentials = new SigningCredentials(new RsaSecurityKey(keyData.Parameters) { KeyId = keyData.Id }, keyData.Algorithm); } - if (!keyMaterial.IsX509Certificate && keyMaterial.Algorithm.StartsWith('E')) + if (!keyMaterial.IsX509Certificate && keyMaterial.Algorithm.IsEcAlgorithm()) { var keyData = JsonSerializer.Deserialize(unprotectedData, Settings); - ECCurve curve = keyMaterial.Algorithm switch - { - "ES256" => CryptoHelper.GetCurveFromCrvValue("P-256"), - "ES384" => CryptoHelper.GetCurveFromCrvValue("P-384"), - "ES521" => CryptoHelper.GetCurveFromCrvValue("P-521"), - _ => throw new ArgumentOutOfRangeException(nameof(keyMaterial.Algorithm), "Unexpected algorithm value for EC Curve") - }; - + ECCurve curve = CryptoHelper.GetCurveFromCrvValue( + keyMaterial.Algorithm.GetCurveNameForAlgorithm()); var ecdsa = ECDsa.Create(new ECParameters { Curve = curve, D = keyData.D, Q = keyData.Q }); signingKey.Created = keyData.Created; diff --git a/src/Open.IdentityServer/src/Extensions/StringsExtensions.cs b/src/Open.IdentityServer/src/Extensions/StringsExtensions.cs index 31f86870b..d12d60a53 100644 --- a/src/Open.IdentityServer/src/Extensions/StringsExtensions.cs +++ b/src/Open.IdentityServer/src/Extensions/StringsExtensions.cs @@ -7,10 +7,13 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Text.Encodings.Web; +#nullable enable + namespace Open.IdentityServer.Extensions; internal static class StringExtensions @@ -47,7 +50,7 @@ public static IEnumerable FromSpaceSeparatedString(this string input) return input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToList(); } - public static List ParseScopesString(this string scopes) + public static List? ParseScopesString(this string? scopes) { if (scopes.IsMissing()) { @@ -67,13 +70,13 @@ public static List ParseScopesString(this string scopes) } [DebuggerStepThrough] - public static bool IsMissing(this string value) + public static bool IsMissing([NotNullWhen(false)] this string? value) { return string.IsNullOrWhiteSpace(value); } [DebuggerStepThrough] - public static bool IsMissingOrTooLong(this string value, int maxLength) + public static bool IsMissingOrTooLong(this string? value, int maxLength) { if (string.IsNullOrWhiteSpace(value)) { @@ -89,13 +92,13 @@ public static bool IsMissingOrTooLong(this string value, int maxLength) } [DebuggerStepThrough] - public static bool IsPresent(this string value) + public static bool IsPresent([NotNullWhen(true)] this string? value) { return !string.IsNullOrWhiteSpace(value); } [DebuggerStepThrough] - public static string EnsureLeadingSlash(this string url) + public static string? EnsureLeadingSlash(this string? url) { if (url != null && !url.StartsWith("/")) { @@ -106,7 +109,7 @@ public static string EnsureLeadingSlash(this string url) } [DebuggerStepThrough] - public static string EnsureTrailingSlash(this string url) + public static string? EnsureTrailingSlash(this string? url) { if (url != null && !url.EndsWith("/")) { @@ -117,7 +120,7 @@ public static string EnsureTrailingSlash(this string url) } [DebuggerStepThrough] - public static string RemoveLeadingSlash(this string url) + public static string? RemoveLeadingSlash(this string? url) { if (url != null && url.StartsWith("/")) { @@ -128,7 +131,7 @@ public static string RemoveLeadingSlash(this string url) } [DebuggerStepThrough] - public static string RemoveTrailingSlash(this string url) + public static string? RemoveTrailingSlash(this string? url) { if (url != null && url.EndsWith("/")) { @@ -139,9 +142,9 @@ public static string RemoveTrailingSlash(this string url) } [DebuggerStepThrough] - public static string CleanUrlPath(this string url) + public static string CleanUrlPath(this string? url) { - if (String.IsNullOrWhiteSpace(url)) url = "/"; + if (string.IsNullOrWhiteSpace(url)) url = "/"; if (url != "/" && url.EndsWith("/")) { @@ -153,7 +156,7 @@ public static string CleanUrlPath(this string url) [DebuggerStepThrough] // Clone of UrlHelperBase.CheckIsLocalUrl from https://github.com/dotnet/aspnetcore/blob/3f1acb59718cadf111a0a796681e3d3509bb3381/src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs - public static bool IsLocalUrl(this string url) + public static bool IsLocalUrl(this string? url) { if (string.IsNullOrEmpty(url)) { @@ -246,7 +249,7 @@ public static string AddHashFragment(this string url, string query) } [DebuggerStepThrough] - public static NameValueCollection ReadQueryStringAsNameValueCollection(this string url) + public static NameValueCollection ReadQueryStringAsNameValueCollection(this string? url) { if (url != null) { @@ -266,7 +269,7 @@ public static NameValueCollection ReadQueryStringAsNameValueCollection(this stri return new NameValueCollection(); } - public static string GetOrigin(this string url) + public static string? GetOrigin(this string? url) { if (url != null) { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Utility/InternalStringExtensions.cs b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Utility/InternalStringExtensions.cs index 06fc17f4d..c3fce0580 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Utility/InternalStringExtensions.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Utility/InternalStringExtensions.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. #nullable enable @@ -11,13 +12,13 @@ namespace IdentityServer.IntegrationTests.Utility; internal static class InternalStringExtensions { [DebuggerStepThrough] - public static bool IsMissing(this string value) + public static bool IsMissing([NotNullWhen(false)] this string? value) { return string.IsNullOrWhiteSpace(value); } [DebuggerStepThrough] - public static bool IsPresent(this string value) + public static bool IsPresent([NotNullWhen(true)] this string? value) { return !(value.IsMissing()); } diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Configuration/CryptoHelperTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Configuration/CryptoHelperTests.cs new file mode 100644 index 000000000..282643c71 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Configuration/CryptoHelperTests.cs @@ -0,0 +1,79 @@ +using AwesomeAssertions; +using Open.IdentityServer.Configuration; +using System; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Configuration; + +public class CryptoHelperTests +{ + [Theory] + [InlineData("RS256")] + [InlineData("RS384")] + [InlineData("RS512")] + [InlineData("PS256")] + [InlineData("PS384")] + [InlineData("PS512")] + public void IsRsaAlgorithm_ShouldReturnTrueForRsaAlgorithms(string algorithm) + { + algorithm.IsRsaAlgorithm().Should().BeTrue(); + } + + [Theory] + [InlineData("ES256")] + [InlineData("ES384")] + [InlineData("ES512")] + [InlineData("HS256")] + [InlineData("AES256")] + public void IsRsaAlgorithm_ShouldReturnFalseForNonRsaAlgorithms(string algorithm) + { + algorithm.IsRsaAlgorithm().Should().BeFalse(); + } + + [Theory] + [InlineData("ES256")] + [InlineData("ES384")] + [InlineData("ES521")] + public void IsEcAlgorithm_ShouldReturnTrueForEcAlgorithms(string algorithm) + { + algorithm.IsEcAlgorithm().Should().BeTrue(); + } + + [Theory] + [InlineData("RS256")] + [InlineData("RS384")] + [InlineData("RS512")] + [InlineData("PS256")] + [InlineData("PS384")] + [InlineData("PS512")] + [InlineData("HS256")] + [InlineData("AES256")] + public void IsEcAlgorithm_ShouldReturnFalseForNonEcAlgorithms(string algorithm) + { + algorithm.IsEcAlgorithm().Should().BeFalse(); + } + + [Theory] + [InlineData("ES256", "P-256")] + [InlineData("ES384", "P-384")] + [InlineData("ES521", "P-521")] + public void GetCurveNameForAlgorithm_ShouldReturnCorrectCurveNameForEcAlgorithms(string algorithm, string expectedCurveName) + { + algorithm.GetCurveNameForAlgorithm().Should().Be(expectedCurveName); + } + + [Theory] + [InlineData("RS256")] + [InlineData("RS384")] + [InlineData("RS512")] + [InlineData("PS256")] + [InlineData("PS384")] + [InlineData("PS512")] + [InlineData("HS256")] + [InlineData("AES256")] + public void GetCurveNameForAlgorithm_ShouldThrowArgumentOutOfRangeExceptionForNonEcAlgorithms(string algorithm) + { + Action act = () => algorithm.GetCurveNameForAlgorithm(); + act.Should().Throw(); + } +} diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Open.IdentityServer.UnitTests.csproj b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Open.IdentityServer.UnitTests.csproj index 46644c63d..50baae4c4 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Open.IdentityServer.UnitTests.csproj +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Open.IdentityServer.UnitTests.csproj @@ -12,18 +12,18 @@ - + - + - - - - - + + + + + @@ -36,6 +36,6 @@ - + diff --git a/src/Storage/src/Extensions/StringsExtensions.cs b/src/Storage/src/Extensions/StringsExtensions.cs index 4aec2bf9b..8c46339be 100644 --- a/src/Storage/src/Extensions/StringsExtensions.cs +++ b/src/Storage/src/Extensions/StringsExtensions.cs @@ -1,21 +1,25 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +#nullable enable namespace Open.IdentityServer.Extensions; internal static class StringExtensions { [DebuggerStepThrough] - public static bool IsMissing(this string value) + public static bool IsMissing([NotNullWhen(false)] this string? value) { return string.IsNullOrWhiteSpace(value); } [DebuggerStepThrough] - public static bool IsPresent(this string value) + public static bool IsPresent([NotNullWhen(true)] this string? value) { return !string.IsNullOrWhiteSpace(value); }