From 8a0a13555c4488c9d75ceb87477e1dc56651de38 Mon Sep 17 00:00:00 2001 From: James Britton Date: Thu, 4 Jun 2026 13:30:03 +0100 Subject: [PATCH 1/6] feat: add options object to configuring compatibility store for configurability --- .../BuilderExtensions/Crypto.cs | 9 +- .../Options/CompatibilityKeyStoreOptions.cs | 14 +++ .../Compatibility/IdentityServerKeyStore.cs | 40 +++++++++ .../IdentityServerSigningCredentialStore.cs | 17 ++-- .../IdentityServerValidationKeysStore.cs | 16 ++-- .../Discovery/FakeIdentityServerKeyStore.cs | 23 +++++ .../Endpoints/Discovery/JwkEndpointTests.cs | 5 ++ ...pen.IdentityServer.IntegrationTests.csproj | 1 + ...entityServerSigningCredentialStoreTests.cs | 72 +++++++++++---- .../IdentityServerValidationKeysStoreTests.cs | 90 +++++++++++++++++-- 10 files changed, 251 insertions(+), 36 deletions(-) create mode 100644 src/Open.IdentityServer/src/Configuration/DependencyInjection/Options/CompatibilityKeyStoreOptions.cs create mode 100644 src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerKeyStore.cs diff --git a/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Crypto.cs b/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Crypto.cs index 57286c1ff..a0e2590a6 100644 --- a/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Crypto.cs +++ b/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Crypto.cs @@ -303,12 +303,17 @@ public static IIdentityServerBuilder AddValidationKey( /// Add the signing and validation key stores that target the IdentityServer compatibility key stores /// /// The builder. + /// Action to configure compatibility key store options /// - public static IIdentityServerBuilder AddCompatibilityKeyStores(this IIdentityServerBuilder builder) + public static IIdentityServerBuilder AddCompatibilityKeyStores(this IIdentityServerBuilder builder, Action configure = null) { + var options = new CompatibilityKeyStoreOptions(); + configure?.Invoke(options); + + builder.Services.AddSingleton(options); builder.Services.AddTransient(); - builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); return builder; } diff --git a/src/Open.IdentityServer/src/Configuration/DependencyInjection/Options/CompatibilityKeyStoreOptions.cs b/src/Open.IdentityServer/src/Configuration/DependencyInjection/Options/CompatibilityKeyStoreOptions.cs new file mode 100644 index 000000000..4675bd375 --- /dev/null +++ b/src/Open.IdentityServer/src/Configuration/DependencyInjection/Options/CompatibilityKeyStoreOptions.cs @@ -0,0 +1,14 @@ +using System; + +namespace Open.IdentityServer.Configuration; + +/// +/// Compatibility key store options. +/// +public class CompatibilityKeyStoreOptions +{ + /// + /// Maximum lifetime for keys read from the key store, defaults to 90 days. + /// + public TimeSpan MaxLifetime { get; set; } = TimeSpan.FromDays(90); +} \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerKeyStore.cs b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerKeyStore.cs new file mode 100644 index 000000000..302e3a6e5 --- /dev/null +++ b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerKeyStore.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Open.IdentityServer.Configuration; +using Open.IdentityServer.DataProtection; +using Open.IdentityServer.Models; + +namespace Open.IdentityServer.Stores; + +/// +/// This class is the base implementation of the IdentityServer compatibility signing key store that retrieves legacy +/// from the existing IdentityServer read-only key store. +/// +/// The key store used to retrieve signing keys. +/// Converter used to decrypt and convert data-protected key material into usable credentials. +/// Time provider implementation +/// Compatibility key store options +public abstract class IdentityServerSigningKeyStore( + IIdentityServerKeyStore identityServerKeyStore, + DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter, + TimeProvider timeProvider, + CompatibilityKeyStoreOptions options) +{ + private readonly IIdentityServerKeyStore identityServerKeyStore = identityServerKeyStore; + private readonly DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter = dataProtectedIdentityServerKeyMaterialConverter; + private readonly CompatibilityKeyStoreOptions options = options; + + /// + /// Get all signing keys from the store, filtering based on the compatibility key store configuration and use + /// + /// + protected IEnumerable GetKeysAsync() + { + return identityServerKeyStore.GetKeys() + .Where(x => x.Use == "signing") + .Select(dataProtectedIdentityServerKeyMaterialConverter.Convert) + .Where(x => x.Created.Add(options.MaxLifetime) > timeProvider.GetUtcNow()) + .OrderByDescending(x => x.Created); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerSigningCredentialStore.cs b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerSigningCredentialStore.cs index 12588c90c..5bc6cd422 100644 --- a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerSigningCredentialStore.cs +++ b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerSigningCredentialStore.cs @@ -1,9 +1,11 @@ // Copyright (c) 2026, Rock Solid Knowledge Ltd // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. +using System; using System.Linq; using System.Threading.Tasks; using Microsoft.IdentityModel.Tokens; +using Open.IdentityServer.Configuration; using Open.IdentityServer.DataProtection; namespace Open.IdentityServer.Stores; @@ -13,20 +15,25 @@ namespace Open.IdentityServer.Stores; /// /// The key store used to retrieve signing keys. /// Converter used to decrypt and convert data-protected key material into usable credentials. +/// Time provider implementation +/// Compatibility key store options public class IdentityServerSigningCredentialStore( IIdentityServerKeyStore identityServerKeyStore, - DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter): ISigningCredentialStore + DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter, + TimeProvider timeProvider, + CompatibilityKeyStoreOptions options + ): + IdentityServerSigningKeyStore(identityServerKeyStore, dataProtectedIdentityServerKeyMaterialConverter, timeProvider, options), + ISigningCredentialStore { + /// /// Gets the key to be used for signing from the key store, will be the newest key /// /// a key to be used for signing public async Task GetSigningCredentialsAsync() { - return identityServerKeyStore.GetKeys() - .Where(x => x.Use == "signing") - .Select(dataProtectedIdentityServerKeyMaterialConverter.Convert) - .OrderByDescending(x => x.Created) + return GetKeysAsync() .Select(x => x.Credentials) .FirstOrDefault(); } diff --git a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerValidationKeysStore.cs b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerValidationKeysStore.cs index 70bd08882..53cb871b8 100644 --- a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerValidationKeysStore.cs +++ b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerValidationKeysStore.cs @@ -1,9 +1,11 @@ // Copyright (c) 2026, Rock Solid Knowledge Ltd // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Open.IdentityServer.Configuration; using Open.IdentityServer.DataProtection; using Open.IdentityServer.Models; @@ -14,9 +16,16 @@ namespace Open.IdentityServer.Stores; /// /// The key store used to retrieve all available signing keys. /// Converter used to decrypt and convert data-protected key material into usable credentials. +/// Time provider implementation +/// Compatibility key store options public class IdentityServerValidationKeysStore( IIdentityServerKeyStore identityServerKeyStore, - DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter): IValidationKeysStore + DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter, + TimeProvider timeProvider, + CompatibilityKeyStoreOptions options + ): + IdentityServerSigningKeyStore(identityServerKeyStore, dataProtectedIdentityServerKeyMaterialConverter, timeProvider, options), + IValidationKeysStore { /// /// Gets all keys to be used for validation in the Duende key store, will be any other keys that are not retired, or the @@ -25,10 +34,7 @@ public class IdentityServerValidationKeysStore( /// list of validation keys info public async Task> GetValidationKeysAsync() { - return identityServerKeyStore.GetKeys() - .Where(x => x.Use == "signing") - .Select(dataProtectedIdentityServerKeyMaterialConverter.Convert) - .OrderByDescending(x => x.Created) + return GetKeysAsync() .Select(x => new SecurityKeyInfo { Key = x.Credentials.Key, SigningAlgorithm = x.Credentials.Algorithm }) .ToList(); } diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/FakeIdentityServerKeyStore.cs b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/FakeIdentityServerKeyStore.cs index 53950c0df..fa7fb80e0 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/FakeIdentityServerKeyStore.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/FakeIdentityServerKeyStore.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; @@ -6,6 +7,8 @@ namespace IdentityServer.IntegrationTests.Endpoints.Discovery; public class FakeIdentityServerKeyStore: IIdentityServerKeyStore { + public static DateTime FakeNow = new DateTime(2026, 02, 01, 12, 00, 00, DateTimeKind.Utc); + public IEnumerable GetKeys() { return @@ -15,6 +18,7 @@ public IEnumerable GetKeys() new IdentityServerKeyMaterial { Id = "FakeRSA_ES256", Version = 1, Use = "signing", DataProtected = false, Algorithm = "ES256", Data = FakeES256KeyData }, new IdentityServerKeyMaterial { Id = "FakeRSA_ES384", Version = 1, Use = "signing", DataProtected = false, Algorithm = "ES384", Data = FakeES384KeyData }, new IdentityServerKeyMaterial { Id = "FakeRSA_ES521", Version = 1, Use = "signing", DataProtected = false, Algorithm = "ES521", Data = FakeES521KeyData }, + new IdentityServerKeyMaterial { Id = "FakeRSA_PS512_Expired", Version = 1, Use = "signing", DataProtected = false, Algorithm = "PS256", Data = FakePS512KeyData }, ]; } @@ -97,4 +101,23 @@ public IEnumerable GetKeys() "Created": "2026-01-01T12:00:00" } """; + + private const string FakePS512KeyData = """ + { + "Parameters": { + "D": "Bj8nuAP4kWK6Bh5u7JM1SlkENF4MmaCDhptjtoG3mo+Blh7EfRb3IGCNP4E81xmWTBAmnNdb6/H9e0YSublgXVHnXcsEwLCiIucWCa9YmwuUeBw4cMzNIHVsWss2wv7XpY18SVLzZs9PW99zgZwREquQFbYcjVGfEQqEYNGe891Lz+AKTo5Qpsyx3laZLTOUL+VBb5bJGJxl2HIeFWDbrn/bqltBRUcjayYiX563IwtHBwLBWBAEbrBBurGPaIk9PjC4WVAvVdvWYZQgksfePsV7/1zWhi6K5VElAn0ohrpmImHWh6ob+nIV6oUJG/gXpNAmB2ZbMDopeRn1QRZKAQ==", + "DP": "QUvY4w84l8MoqhAcxoXA7H6DaPZRpDnoNuTPfMVJCjnMxBLrX8k2Yx8vPk2KhFXSklG9K0PrLWsp8EO/dHVplbUXqhluYbeAJ6UZXNXW1cmjw6AXiYn+SwibP7KY6HhElxTVqhMqm+66e6Coei11otsweRMWT3dSmPBMlYoxc/M=", + "DQ": "2HtvEUst8qBP7yl1n2cMoEBNo6X6Hv8On2oFBvAtsUgzfHO9BKRdrrwo7fGH6t6ZGoGYB+uE9tbenRpr58ZEHHE65bKwZ6z/Us6ogKq/QOsfroQCU509U0x5aXn3DfYwzNGXDDlw7RESh89L9YWf7j9UsMeITPpw1nINf06jIAE=", + "Exponent": "AQAB", + "InverseQ": "PUaNIRe81tN+RmxtA4OrfKupweBWPfGrKXosT9IbIlP3aqEGsR0+rLtWzs+5fw0bMyFSLcVUmEQAfso/RirlskqtO0A5Zg2qfQjSVZkTCvLrPYBnu94JwNhA6K/MawkoRcqsM682FVOlJvSqkhbCZuG/6AcF+Lu3To4oDjsm5y4=", + "Modulus": "zYRdc59usklvJRIgQhK+6jEp0P8YohIj3vYEWyv3S/dHqa8dSGloyHDJhG6RAdqnR8S2nhGTcdokqRE4hSdWMaQSd5FKgC2ZpKe0BM3qt4q7lzid8kezkUqkhqCV39ITEiMxMkeMDK+SZqiH2OB5J6n+G4b5PnroshElcNXRaWultIm+vnAWkPXJyhc0mvD+uAlFd9TMbTU5BZE7NZ0tn8Y0Ny2MAPLWW0UVmkGeSwS5rCROiaJTS3P50BJD5eAJrq4OAilVjciBWWt68+6uwxmUGZye9foqAc98P3jLM7ccLrMKklyejTBNIcisyNEY+3+WM1NkA3mMy0qVS0cbXw==", + "P": "7khqnAiMQG17a5daoEmL2zcB6rpnJ5t7cVqJd12UUCbfVzhP+pdxvjG5vt4Dpb5/lN29GlC+sJeL9WsmBSwtCENb1No3uUQ8QbQYCFA4gBInck5q2RBRuPeqP4RHJWglR+9rDSs+hcGvNaDPBL70BCyUQKcrlaBSy4NDwBCFZb8=", + "Q": "3MxE5TRElLXPnY0miYxqGc3zpjKayu/c7yRaM/kR+16+fRi1AWhscVBpusA27xHdS5SZiTGEeLdqiKQ+OVC/oNmXS2aXDaGOyb6014jsil43sBQVQekeiTfkKslPE03DlOr6w6KqeuiKTF+yQBHvH3yybDlXy3wqpiGLq7az8mE=" + }, + "Id": "FakeRSA_PS512_Expired", + "Algorithm": "PS512", + "HasX509Certificate": false, + "Created": "2025-10-01T12:00:00" + } + """; } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/JwkEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/JwkEndpointTests.cs index 05a1ef11c..665eaa231 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/JwkEndpointTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/JwkEndpointTests.cs @@ -1,12 +1,14 @@ // Copyright (c) 2026, Rock Solid Knowledge Ltd // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. +using System; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using AwesomeAssertions; using IdentityServer.IntegrationTests.Common; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Time.Testing; using Microsoft.IdentityModel.Tokens; using Open.IdentityServer; using Open.IdentityServer.Configuration; @@ -157,9 +159,12 @@ public async Task Jwks_with_two_key_using_different_algs_expect_different_alg_va public async Task Jwks_WhenCompatibilityStoreConfigured_ShouldGetKeysFromStore() { IdentityServerPipeline pipeline = new IdentityServerPipeline(); + FakeTimeProvider timeProvider = new FakeTimeProvider(); + timeProvider.SetUtcNow(FakeIdentityServerKeyStore.FakeNow); pipeline.OnPostConfigureServices += services => { + services.AddSingleton(timeProvider); services.AddScoped(); services.AddIdentityServerBuilder() .AddCompatibilityKeyStores(); diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Open.IdentityServer.IntegrationTests.csproj b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Open.IdentityServer.IntegrationTests.csproj index 242a19ac0..f280fc5d8 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Open.IdentityServer.IntegrationTests.csproj +++ b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Open.IdentityServer.IntegrationTests.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs index 302e18a60..d725d430c 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using AwesomeAssertions; using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Time.Testing; using Microsoft.IdentityModel.Tokens; using Moq; using Open.IdentityServer.Configuration; @@ -21,16 +22,22 @@ namespace IdentityServer.UnitTests.Stores.Compatibility; public class IdentityServerSigningCredentialStoreTests { - private readonly IIdentityServerKeyStore _identityServerKeyStore = Mock.Of(); + private readonly IIdentityServerKeyStore identityServerKeyStore = Mock.Of(); private readonly IDataProtectionProvider dataProtectionProvider = Mock.Of(); private readonly IDataProtector dataProtector = Mock.Of(); - private readonly DataProtectedIdentityServerKeyMaterialConverter _dataProtectedIdentityServerKeyMaterialConverter; + private readonly DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter; + private readonly FakeTimeProvider timeProvider = new(); + private readonly CompatibilityKeyStoreOptions fakeOptions = new(); + + private DateTime FakeNow = new(2026, 05, 01, 12, 00, 00, DateTimeKind.Utc); private List fakeKeyMaterials = []; public IdentityServerSigningCredentialStoreTests() { - Mock.Get(_identityServerKeyStore) + timeProvider.SetUtcNow(FakeNow); + + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns(fakeKeyMaterials); @@ -38,10 +45,10 @@ public IdentityServerSigningCredentialStoreTests() .Setup(x => x.CreateProtector("DataProtectionKeyProtector")) .Returns(dataProtector); - _dataProtectedIdentityServerKeyMaterialConverter = new DataProtectedIdentityServerKeyMaterialConverter(dataProtectionProvider); + dataProtectedIdentityServerKeyMaterialConverter = new DataProtectedIdentityServerKeyMaterialConverter(dataProtectionProvider); } - private IdentityServerSigningCredentialStore CreateSut() => new(_identityServerKeyStore, _dataProtectedIdentityServerKeyMaterialConverter); + private IdentityServerSigningCredentialStore CreateSut() => new(identityServerKeyStore, dataProtectedIdentityServerKeyMaterialConverter, timeProvider, fakeOptions); [Fact] public async Task GetSigningCredentialsAsync_WhenDataUnprotected_AndLatestRsaKey_ShouldReturnSigningCredentialsRepresenting() @@ -68,7 +75,7 @@ public async Task GetSigningCredentialsAsync_WhenDataUnprotected_AndLatestRsaKey new IdentityServerKeyMaterial { Id = fakeRsaKey.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeRsaKey.Algorithm, Data = fakeRsaKey.ToExpectedJson() }, ]; - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns(fakeKeyMaterials); @@ -105,7 +112,7 @@ public async Task GetSigningCredentialsAsync_ShouldFilterOutNonSigningKeys() new IdentityServerKeyMaterial { Id = fakeRsaKey.Id, Version = 1, Use = "encryption", DataProtected = false, Algorithm = fakeRsaKey.Algorithm, Data = fakeRsaKey.ToExpectedJson() }, ]; - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns(fakeKeyMaterials); @@ -121,6 +128,41 @@ public async Task GetSigningCredentialsAsync_ShouldFilterOutNonSigningKeys() actual.Should().BeEquivalentTo(expectedSigningCredentials); } + [Fact] + public async Task GetSigningCredentialsAsync_ShouldFilterOutKeysThatExceedMaxLifetime() + { + var fakeRsaKey = new RsaIdentityServerKeyData + { + Id = "Fake_RS256", + Created = FakeNow.AddDays(-130), + Algorithm = "RS256", + Parameters = FakeKeyData.RsaSecurityKey256, + }; + + var fakeEcKey = new EcIdentityServerKeyData + { + Id = "Fake_ES256", + Created = FakeNow.AddDays(-99), + Algorithm = "ES256", + D = FakeKeyData.EcDsaSecurityKey256.D, + Q = FakeKeyData.EcDsaSecurityKey256.Q, + }; + + fakeKeyMaterials = [ + new IdentityServerKeyMaterial { Id = fakeEcKey.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeEcKey.Algorithm, Data = fakeEcKey.ToExpectedJson() }, + new IdentityServerKeyMaterial { Id = fakeRsaKey.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeRsaKey.Algorithm, Data = fakeRsaKey.ToExpectedJson() }, + ]; + + Mock.Get(identityServerKeyStore) + .Setup(x => x.GetKeys()) + .Returns(fakeKeyMaterials); + + var sut = CreateSut(); + var actual = await sut.GetSigningCredentialsAsync(); + + actual.Should().BeNull(); + } + [Fact] public async Task GetSigningCredentialsAsync_WhenDataProtected_AndLatestEcdsaKey_ShouldReturnSigningCredentialsRepresenting() { @@ -150,7 +192,7 @@ public async Task GetSigningCredentialsAsync_WhenDataProtected_AndLatestEcdsaKey new IdentityServerKeyMaterial { Id = fakeRsaKey.Id, Version = 1, Use = "signing", DataProtected = true, Algorithm = fakeRsaKey.Algorithm, Data = fakeRsaKeyJsonProtectedBase64 }, ]; - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns(fakeKeyMaterials); @@ -194,7 +236,7 @@ public async Task GetSigningCredentialsAsync_WhenProtected_AndSingleEcdsa384Key_ new IdentityServerKeyMaterial { Id = fakeEcKey.Id, Version = 1, Use = "signing", DataProtected = true, Algorithm = fakeEcKey.Algorithm, Data = fakeEcKeyJsonProtectedBase64 }, ]; - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns(fakeKeyMaterials); @@ -232,7 +274,7 @@ public async Task GetSigningCredentialsAsync_WhenUnProtected_AndSingleEcdsa521Ke new IdentityServerKeyMaterial { Id = fakeEcKey.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeEcKey.Algorithm, Data = fakeEcKeyJson }, ]; - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns(fakeKeyMaterials); @@ -258,7 +300,7 @@ public async Task GetSigningCredentialsAsync_WhenProtected_AndSingleRsaKeyWrappe var fakeX509CertData = new X509IdentityServerKeyData { Id = fakeRsaSecurityKey.KeyId, - Created = new DateTime(2026, 02, 25, 11, 20, 21, DateTimeKind.Utc), + Created = FakeNow.AddDays(-33), Algorithm = "RS256", CertificateRawData = fakeRsaSecurityKey.ToBase64Pfx(), }; @@ -269,7 +311,7 @@ public async Task GetSigningCredentialsAsync_WhenProtected_AndSingleRsaKeyWrappe new IdentityServerKeyMaterial { Id = fakeX509CertData.Id, Version = 1, Use = "signing", DataProtected = true, Algorithm = fakeX509CertData.Algorithm, IsX509Certificate = true, Data = fakeX509PfxProtectedBase64 }, ]; - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns(fakeKeyMaterials); @@ -297,7 +339,7 @@ public async Task GetSigningCredentialsAsync_WhenProtected_AndSingleECDsaKeyWrap var fakeX509CertData = new X509IdentityServerKeyData { Id = fakeECDsaSecurityKey.KeyId, - Created = new DateTime(2026, 02, 25, 11, 20, 21, DateTimeKind.Utc), + Created = FakeNow.AddDays(-33), Algorithm = "EC384", CertificateRawData = fakeECDsaSecurityKey.ToBase64Pfx(), }; @@ -308,7 +350,7 @@ public async Task GetSigningCredentialsAsync_WhenProtected_AndSingleECDsaKeyWrap new IdentityServerKeyMaterial { Id = fakeX509CertData.Id, Version = 1, Use = "signing", DataProtected = true, Algorithm = fakeX509CertData.Algorithm, IsX509Certificate = true, Data = fakeX509PfxProtectedBase64 }, ]; - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns(fakeKeyMaterials); @@ -329,7 +371,7 @@ public async Task GetSigningCredentialsAsync_WhenProtected_AndSingleECDsaKeyWrap [Fact] public async Task GetSigningCredentialsAsync_WhenTableIsEmpty_ShouldReturnNull() { - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns([]); diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs index 6f0ae28c5..a9cfd713d 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using AwesomeAssertions; using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Time.Testing; using Microsoft.IdentityModel.Tokens; using Moq; using Open.IdentityServer.Configuration; @@ -23,11 +24,17 @@ public class IdentityServerValidationKeysStoreTests private readonly IDataProtectionProvider dataProtectionProvider = Mock.Of(); private IDataProtector dataProtector = Mock.Of(); private readonly DataProtectedIdentityServerKeyMaterialConverter _dataProtectedIdentityServerKeyMaterialConverter; + private readonly FakeTimeProvider timeProvider = new(); + private readonly CompatibilityKeyStoreOptions fakeOptions = new(); + + private DateTime FakeNow = new DateTime(2026, 05, 01, 12, 00, 00, DateTimeKind.Utc); private List fakeKeyMaterials = []; public IdentityServerValidationKeysStoreTests() { + timeProvider.SetUtcNow(FakeNow); + _dataProtectedIdentityServerKeyMaterialConverter = new DataProtectedIdentityServerKeyMaterialConverter(dataProtectionProvider); Mock.Get(_identityServerKeyStore) @@ -39,7 +46,7 @@ public IdentityServerValidationKeysStoreTests() .Returns(dataProtector); } - private IdentityServerValidationKeysStore CreateSut() => new(_identityServerKeyStore, _dataProtectedIdentityServerKeyMaterialConverter); + private IdentityServerValidationKeysStore CreateSut() => new(_identityServerKeyStore, _dataProtectedIdentityServerKeyMaterialConverter, timeProvider, fakeOptions); [Fact] public async Task GetValidationKeysAsync_ShouldReturnCollectionOfSecurityKeyInfo() @@ -47,7 +54,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionOfSecurityKeyInfo var fakeRsaKey0 = new RsaIdentityServerKeyData { Id = "Fake_RS256_0", - Created = new DateTime(2026, 04, 25, 11, 20, 21, DateTimeKind.Utc), + Created = FakeNow.AddDays(-20), Algorithm = "RS256", Parameters = FakeKeyData.RsaSecurityKey256, }; @@ -55,7 +62,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionOfSecurityKeyInfo var fakeRsaKey1 = new RsaIdentityServerKeyData { Id = "Fake_RS256_1", - Created = new DateTime(2026, 03, 05, 11, 20, 21, DateTimeKind.Utc), + Created = FakeNow.AddDays(-10), Algorithm = "RS256", Parameters = FakeKeyData.RsaSecurityKey256, }; @@ -63,7 +70,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionOfSecurityKeyInfo var fakeEcKey0 = new EcIdentityServerKeyData { Id = "Fake_ES384_0", - Created = new DateTime(2026, 02, 19, 11, 20, 21, DateTimeKind.Utc), + Created = FakeNow.AddDays(-30), Algorithm = "ES384", D = FakeKeyData.EcDsaSecurityKey384.D, Q = FakeKeyData.EcDsaSecurityKey384.Q, @@ -72,7 +79,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionOfSecurityKeyInfo var fakeEcKey1 = new EcIdentityServerKeyData { Id = "Fake_ES521_0", - Created = new DateTime(2026, 01, 15, 11, 20, 21, DateTimeKind.Utc), + Created = FakeNow.AddDays(-45), Algorithm = "ES521", D = FakeKeyData.EcDsaSecurityKey521.D, Q = FakeKeyData.EcDsaSecurityKey521.Q, @@ -119,7 +126,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A var fakeRsaKey0 = new RsaIdentityServerKeyData { Id = "Fake_RS256_0", - Created = new DateTime(2026, 04, 25, 11, 20, 21, DateTimeKind.Utc), + Created = FakeNow.AddDays(-10), Algorithm = "RS256", Parameters = FakeKeyData.RsaSecurityKey256, }; @@ -127,7 +134,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A var fakeRsaKey1 = new RsaIdentityServerKeyData { Id = "Fake_RS256_1", - Created = new DateTime(2026, 03, 05, 11, 20, 21, DateTimeKind.Utc), + Created = FakeNow.AddDays(-30), Algorithm = "RS256", Parameters = FakeKeyData.RsaSecurityKey256, }; @@ -135,7 +142,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A var fakeEcKey0 = new EcIdentityServerKeyData { Id = "Fake_ES256_0", - Created = new DateTime(2026, 02, 19, 11, 20, 21, DateTimeKind.Utc), + Created = FakeNow.AddHours(-12), Algorithm = "ES256", D = FakeKeyData.EcDsaSecurityKey256.D, Q = FakeKeyData.EcDsaSecurityKey256.Q, @@ -144,7 +151,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A var fakeEcKey1 = new EcIdentityServerKeyData { Id = "Fake_ES521_0", - Created = new DateTime(2026, 01, 15, 11, 20, 21, DateTimeKind.Utc), + Created = FakeNow.AddDays(-20), Algorithm = "ES521", D = FakeKeyData.EcDsaSecurityKey521.D, Q = FakeKeyData.EcDsaSecurityKey521.Q, @@ -178,6 +185,71 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A actual.Should().BeEquivalentTo(expectedCredentials); } + [Fact] + public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_AndFilterOutKeysThatExceedMaxLifetime() + { + var fakeRsaKey0 = new RsaIdentityServerKeyData + { + Id = "Fake_RS256_0", + Created = FakeNow.AddDays(-30), + Algorithm = "RS256", + Parameters = FakeKeyData.RsaSecurityKey256, + }; + + var fakeRsaKey1 = new RsaIdentityServerKeyData + { + Id = "Fake_RS256_1", + Created = FakeNow.AddHours(-12), + Algorithm = "RS256", + Parameters = FakeKeyData.RsaSecurityKey256, + }; + + var fakeEcKey0 = new EcIdentityServerKeyData + { + Id = "Fake_ES256_0", + Created = FakeNow.AddDays(-100), + Algorithm = "ES256", + D = FakeKeyData.EcDsaSecurityKey256.D, + Q = FakeKeyData.EcDsaSecurityKey256.Q, + }; + + var fakeEcKey1 = new EcIdentityServerKeyData + { + Id = "Fake_ES521_0", + Created = FakeNow.AddDays(-10), + Algorithm = "ES521", + D = FakeKeyData.EcDsaSecurityKey521.D, + Q = FakeKeyData.EcDsaSecurityKey521.Q, + }; + + fakeKeyMaterials = [ + new IdentityServerKeyMaterial { Id = fakeRsaKey0.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeRsaKey0.Algorithm, Data = fakeRsaKey0.ToExpectedJson() }, + new IdentityServerKeyMaterial { Id = fakeRsaKey1.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeRsaKey1.Algorithm, Data = fakeRsaKey1.ToExpectedJson() }, + new IdentityServerKeyMaterial { Id = fakeEcKey0.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeEcKey0.Algorithm, Data = fakeEcKey0.ToExpectedJson() }, + new IdentityServerKeyMaterial { Id = fakeEcKey1.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeEcKey1.Algorithm, Data = fakeEcKey1.ToExpectedJson() }, + ]; + + Mock.Get(_identityServerKeyStore) + .Setup(x => x.GetKeys()) + .Returns(fakeKeyMaterials); + + var sut = CreateSut(); + var actual = await sut.GetValidationKeysAsync(); + + ECDsa expectedEcDsa1 = ECDsa.Create(new ECParameters + { + Curve = CryptoHelper.GetCurveFromCrvValue("P-521"), D = fakeEcKey1.D, Q = fakeEcKey1.Q, + }); + + IEnumerable expectedCredentials = [ + new() { Key = new RsaSecurityKey(fakeRsaKey0.Parameters) { KeyId = fakeRsaKey0.Id }, SigningAlgorithm = fakeRsaKey0.Algorithm }, + new() { Key = new RsaSecurityKey(fakeRsaKey1.Parameters) { KeyId = fakeRsaKey1.Id }, SigningAlgorithm = fakeRsaKey1.Algorithm }, + new() { Key = new ECDsaSecurityKey(expectedEcDsa1) { KeyId = fakeEcKey1.Id }, SigningAlgorithm = fakeEcKey1.Algorithm }, + ]; + + actual.Should().BeEquivalentTo(expectedCredentials); + } + [Fact] public async Task GetValidationKeysAsync_WhenTableIsEmpty_ShouldReturnEmptyList() { From b5ccbc1bcbc75797f899af7dcb7387219af9e42a Mon Sep 17 00:00:00 2001 From: James Britton Date: Thu, 4 Jun 2026 15:26:32 +0100 Subject: [PATCH 2/6] fix: bug not mapping created value on X509 cert signing keys --- ...ectedIdentityServerKeyMaterialConverter.cs | 6 +- .../src/DataProtectionConstants.cs | 13 ++ ...IdentityServerKeyMaterialConverterTests.cs | 170 ++++++++++++++++++ 3 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 src/Open.IdentityServer/src/DataProtectionConstants.cs create mode 100644 src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedIdentityServerKeyMaterialConverterTests.cs diff --git a/src/Open.IdentityServer/src/DataProtection/DataProtectedIdentityServerKeyMaterialConverter.cs b/src/Open.IdentityServer/src/DataProtection/DataProtectedIdentityServerKeyMaterialConverter.cs index f16e9aa9f..739755634 100644 --- a/src/Open.IdentityServer/src/DataProtection/DataProtectedIdentityServerKeyMaterialConverter.cs +++ b/src/Open.IdentityServer/src/DataProtection/DataProtectedIdentityServerKeyMaterialConverter.cs @@ -18,7 +18,7 @@ namespace Open.IdentityServer.DataProtection; /// The data protection provider used to create a protector for unprotecting encrypted key material. public class DataProtectedIdentityServerKeyMaterialConverter(IDataProtectionProvider dataProtectionProvider) { - private IDataProtector dataProtector = dataProtectionProvider.CreateProtector("DataProtectionKeyProtector"); + private IDataProtector dataProtector = dataProtectionProvider.CreateProtector(DataProtectionConstants.KeyProtectorPurpose); /// /// Json serializer settings @@ -58,8 +58,6 @@ public SigningKey Convert(IdentityServerKeyMaterial keyMaterial) { var keyData = JsonSerializer.Deserialize(unprotectedData, Settings); - signingKey.Created = keyData.Created; - ECCurve curve = keyMaterial.Algorithm switch { "ES256" => CryptoHelper.GetCurveFromCrvValue("P-256"), @@ -70,6 +68,7 @@ public SigningKey Convert(IdentityServerKeyMaterial keyMaterial) var ecdsa = ECDsa.Create(new ECParameters { Curve = curve, D = keyData.D, Q = keyData.Q }); + signingKey.Created = keyData.Created; signingKey.Credentials = new SigningCredentials(new ECDsaSecurityKey(ecdsa) { KeyId = keyData.Id }, keyData.Algorithm); } @@ -79,6 +78,7 @@ public SigningKey Convert(IdentityServerKeyMaterial keyMaterial) var cert = X509CertificateLoader.LoadPkcs12(System.Convert.FromBase64String(keyData.CertificateRawData), null); + signingKey.Created = keyData.Created; signingKey.Credentials = new SigningCredentials(new X509SecurityKey(cert), keyData.Algorithm); } diff --git a/src/Open.IdentityServer/src/DataProtectionConstants.cs b/src/Open.IdentityServer/src/DataProtectionConstants.cs new file mode 100644 index 000000000..9f9433fde --- /dev/null +++ b/src/Open.IdentityServer/src/DataProtectionConstants.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +namespace Open.IdentityServer; + +/// +/// Constants for DataProtection in Open.IdentityServer. +/// +public abstract class DataProtectionConstants +{ + /// Purposed used when creating key material data protector + public const string KeyProtectorPurpose = "DataProtectionKeyProtector"; +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedIdentityServerKeyMaterialConverterTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedIdentityServerKeyMaterialConverterTests.cs new file mode 100644 index 000000000..e04a39d04 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedIdentityServerKeyMaterialConverterTests.cs @@ -0,0 +1,170 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using AwesomeAssertions; +using IdentityServer.UnitTests.Stores.Compatibility; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.IdentityModel.Tokens; +using Moq; +using Open.IdentityServer; +using Open.IdentityServer.Configuration; +using Open.IdentityServer.DataProtection; +using Open.IdentityServer.Models; +using Xunit; + +namespace IdentityServer.UnitTests.DataProtection; + +public class DataProtectedIdentityServerKeyMaterialConverterTests +{ + private readonly IDataProtectionProvider dataProtectionProvider = Mock.Of(); + private readonly IDataProtector dataProtector = Mock.Of(); + + private DataProtectedIdentityServerKeyMaterialConverter CreateSut() => new(dataProtectionProvider); + + public DataProtectedIdentityServerKeyMaterialConverterTests() + { + Mock.Get(dataProtectionProvider) + .Setup(x => x.CreateProtector(DataProtectionConstants.KeyProtectorPurpose)) + .Returns(dataProtector); + } + + [Fact] + public void Ctor_ShouldCreateProtectorWithCorrectName() + { + _ = CreateSut(); + + Mock.Get(dataProtectionProvider) + .Verify(x => x.CreateProtector(DataProtectionConstants.KeyProtectorPurpose)); + } + + [Fact] + public void Convert_WhenRsaIdentityServerKeyData_ShouldConvertToSigningKey() + { + RsaIdentityServerKeyData fakeRsaKey = new() + { + Id = "Fake_RS256", + Created = new DateTime(2026, 04, 25, 11, 20, 21, DateTimeKind.Utc), + Algorithm = "RS256", + Parameters = FakeKeyData.RsaSecurityKey256, + }; + IdentityServerKeyMaterial testKeyMaterial = new() + { + Id = fakeRsaKey.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeRsaKey.Algorithm, + Data = fakeRsaKey.ToExpectedJson() + }; + + DataProtectedIdentityServerKeyMaterialConverter sut = CreateSut(); + SigningKey actual = sut.Convert(testKeyMaterial); + + SigningCredentials expectedCredentials = new(new RsaSecurityKey(fakeRsaKey.Parameters) { KeyId = fakeRsaKey.Id }, fakeRsaKey.Algorithm); + + actual.Id.Should().Be(fakeRsaKey.Id); + actual.Created.Should().Be(fakeRsaKey.Created); + actual.Credentials.Should().BeEquivalentTo(expectedCredentials); + } + + [Fact] + public void Convert_WhenEcIdentityServerKeyData_ShouldConvertToSigningKey() + { + EcIdentityServerKeyData fakeEcKey = new EcIdentityServerKeyData + { + Id = "Fake_ES256", + Created = new DateTime(2026, 02, 25, 11, 20, 21, DateTimeKind.Utc), + Algorithm = "ES256", + D = FakeKeyData.EcDsaSecurityKey256.D, + Q = FakeKeyData.EcDsaSecurityKey256.Q, + }; + IdentityServerKeyMaterial testKeyMaterial = new IdentityServerKeyMaterial + { + Id = fakeEcKey.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeEcKey.Algorithm, + Data = fakeEcKey.ToExpectedJson() + }; + + DataProtectedIdentityServerKeyMaterialConverter sut = CreateSut(); + SigningKey actual = sut.Convert(testKeyMaterial); + + ECDsa expectedEcDsa = ECDsa.Create(new ECParameters + { + Curve = CryptoHelper.GetCurveFromCrvValue("P-256"), D = fakeEcKey.D, Q = fakeEcKey.Q, + }); + SigningCredentials expectedCredentials = new(new ECDsaSecurityKey(expectedEcDsa) { KeyId = fakeEcKey.Id }, fakeEcKey.Algorithm); + + actual.Id.Should().Be(fakeEcKey.Id); + actual.Created.Should().Be(fakeEcKey.Created); + actual.Credentials.Should().BeEquivalentTo(expectedCredentials); + } + + [Fact] + public void Convert_WhenX509IdentityServerKeyData_ShouldConvertToSigningKey() + { + ECDsaSecurityKey fakeECDsaSecurityKey = new ECDsaSecurityKey(ECDsa.Create(FakeKeyData.EcDsaSecurityKey384)) + { + KeyId = "Fake_EC384", + }; + X509IdentityServerKeyData fakeX509CertData = new X509IdentityServerKeyData + { + Id = fakeECDsaSecurityKey.KeyId, + Created = new DateTime(2026, 04, 25, 11, 20, 21, DateTimeKind.Utc), + Algorithm = "EC384", + CertificateRawData = fakeECDsaSecurityKey.ToBase64Pfx(), + }; + IdentityServerKeyMaterial testKeyMaterial = new IdentityServerKeyMaterial + { + Id = fakeX509CertData.Id, Version = 1, Use = "signing", DataProtected = false, + Algorithm = fakeX509CertData.Algorithm, IsX509Certificate = true, Data = fakeX509CertData.ToExpectedJson(), + }; + + DataProtectedIdentityServerKeyMaterialConverter sut = CreateSut(); + SigningKey actual = sut.Convert(testKeyMaterial); + + var expectedCert = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(fakeX509CertData.CertificateRawData), null); + SigningCredentials expectedCredentials = new SigningCredentials(new X509SecurityKey(expectedCert), fakeX509CertData.Algorithm); + + actual.Id.Should().Be(fakeX509CertData.Id); + actual.Created.Should().Be(fakeX509CertData.Created); + actual.Credentials.Should().BeEquivalentTo(expectedCredentials); + } + + [Fact] + public void Convert_WhenDatProtected_ShouldConvertToSigningKey() + { + ECDsaSecurityKey fakeECDsaSecurityKey = new ECDsaSecurityKey(ECDsa.Create(FakeKeyData.EcDsaSecurityKey384)) + { + KeyId = "Fake_EC384", + }; + X509IdentityServerKeyData fakeX509CertData = new X509IdentityServerKeyData + { + Id = fakeECDsaSecurityKey.KeyId, + Created = new DateTime(2026, 04, 25, 11, 20, 21, DateTimeKind.Utc), + Algorithm = "EC384", + CertificateRawData = fakeECDsaSecurityKey.ToBase64Pfx(), + }; + var fakeX509Json = fakeX509CertData.ToExpectedJson(); + var fakeX509PfxProtectedBase64 = Convert.ToBase64String("FakeRsaKeyJson_PROTECTED"u8.ToArray()); + + Mock.Get(dataProtector) + .Setup(x => x.Unprotect(It.Is( + b => Convert.ToBase64String(b) == fakeX509PfxProtectedBase64))) + .Returns(Encoding.UTF8.GetBytes(fakeX509Json)); + + IdentityServerKeyMaterial testKeyMaterial = new IdentityServerKeyMaterial + { + Id = fakeX509CertData.Id, Version = 1, Use = "signing", DataProtected = true, + Algorithm = fakeX509CertData.Algorithm, IsX509Certificate = true, Data = fakeX509PfxProtectedBase64, + }; + + DataProtectedIdentityServerKeyMaterialConverter sut = CreateSut(); + SigningKey actual = sut.Convert(testKeyMaterial); + + var expectedCert = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(fakeX509CertData.CertificateRawData), null); + SigningCredentials expectedCredentials = new SigningCredentials(new X509SecurityKey(expectedCert), fakeX509CertData.Algorithm); + + actual.Id.Should().Be(fakeX509CertData.Id); + actual.Created.Should().Be(fakeX509CertData.Created); + actual.Credentials.Should().BeEquivalentTo(expectedCredentials); + } +} \ No newline at end of file From 28326af1ef4eae75734e74a13a8461056e7e56a1 Mon Sep 17 00:00:00 2001 From: James Britton Date: Fri, 5 Jun 2026 10:56:51 +0100 Subject: [PATCH 3/6] fix: mapped key id correctly for X509 cert wrapped keys --- ...ectedIdentityServerKeyMaterialConverter.cs | 2 +- ...IdentityServerKeyMaterialConverterTests.cs | 47 +++++++++++++++---- .../Stores/Compatibility/FakeKeyData.cs | 22 +++++---- ...entityServerSigningCredentialStoreTests.cs | 8 ++-- 4 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/Open.IdentityServer/src/DataProtection/DataProtectedIdentityServerKeyMaterialConverter.cs b/src/Open.IdentityServer/src/DataProtection/DataProtectedIdentityServerKeyMaterialConverter.cs index 739755634..44e2950c1 100644 --- a/src/Open.IdentityServer/src/DataProtection/DataProtectedIdentityServerKeyMaterialConverter.cs +++ b/src/Open.IdentityServer/src/DataProtection/DataProtectedIdentityServerKeyMaterialConverter.cs @@ -79,7 +79,7 @@ public SigningKey Convert(IdentityServerKeyMaterial keyMaterial) var cert = X509CertificateLoader.LoadPkcs12(System.Convert.FromBase64String(keyData.CertificateRawData), null); signingKey.Created = keyData.Created; - signingKey.Credentials = new SigningCredentials(new X509SecurityKey(cert), keyData.Algorithm); + signingKey.Credentials = new SigningCredentials(new X509SecurityKey(cert) { KeyId = keyData.Id }, keyData.Algorithm); } return signingKey; diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedIdentityServerKeyMaterialConverterTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedIdentityServerKeyMaterialConverterTests.cs index e04a39d04..44c9159b0 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedIdentityServerKeyMaterialConverterTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedIdentityServerKeyMaterialConverterTests.cs @@ -99,17 +99,48 @@ public void Convert_WhenEcIdentityServerKeyData_ShouldConvertToSigningKey() } [Fact] - public void Convert_WhenX509IdentityServerKeyData_ShouldConvertToSigningKey() + public void Convert_WhenX509IdentityServerKeyData_ContainingRsaKey_ShouldConvertToSigningKey() + { + RsaSecurityKey fakeRsaSecurityKey = new RsaSecurityKey(FakeKeyData.RsaSecurityKey256) + { + KeyId = "Fake_RS256", + }; + X509IdentityServerKeyData fakeX509CertData = new X509IdentityServerKeyData + { + Id = fakeRsaSecurityKey.KeyId, + Created = new DateTime(2026, 04, 25, 11, 20, 21, DateTimeKind.Utc), + Algorithm = "RS256", + CertificateRawData = fakeRsaSecurityKey.ToBase64Pfx(), + }; + IdentityServerKeyMaterial testKeyMaterial = new IdentityServerKeyMaterial + { + Id = fakeX509CertData.Id, Version = 1, Use = "signing", DataProtected = false, + Algorithm = fakeX509CertData.Algorithm, IsX509Certificate = true, Data = fakeX509CertData.ToExpectedJson(), + }; + + DataProtectedIdentityServerKeyMaterialConverter sut = CreateSut(); + SigningKey actual = sut.Convert(testKeyMaterial); + + var expectedCert = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(fakeX509CertData.CertificateRawData), null); + SigningCredentials expectedCredentials = new SigningCredentials(new X509SecurityKey(expectedCert) { KeyId = fakeX509CertData.Id }, fakeX509CertData.Algorithm); + + actual.Id.Should().Be(fakeX509CertData.Id); + actual.Created.Should().Be(fakeX509CertData.Created); + actual.Credentials.Should().BeEquivalentTo(expectedCredentials); + } + + [Fact] + public void Convert_WhenX509IdentityServerKeyData_ContainingECDsaKey_ShouldConvertToSigningKey() { ECDsaSecurityKey fakeECDsaSecurityKey = new ECDsaSecurityKey(ECDsa.Create(FakeKeyData.EcDsaSecurityKey384)) { - KeyId = "Fake_EC384", + KeyId = "Fake_ES384", }; X509IdentityServerKeyData fakeX509CertData = new X509IdentityServerKeyData { Id = fakeECDsaSecurityKey.KeyId, Created = new DateTime(2026, 04, 25, 11, 20, 21, DateTimeKind.Utc), - Algorithm = "EC384", + Algorithm = "ES384", CertificateRawData = fakeECDsaSecurityKey.ToBase64Pfx(), }; IdentityServerKeyMaterial testKeyMaterial = new IdentityServerKeyMaterial @@ -122,7 +153,7 @@ public void Convert_WhenX509IdentityServerKeyData_ShouldConvertToSigningKey() SigningKey actual = sut.Convert(testKeyMaterial); var expectedCert = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(fakeX509CertData.CertificateRawData), null); - SigningCredentials expectedCredentials = new SigningCredentials(new X509SecurityKey(expectedCert), fakeX509CertData.Algorithm); + SigningCredentials expectedCredentials = new SigningCredentials(new X509SecurityKey(expectedCert) { KeyId = fakeX509CertData.Id }, fakeX509CertData.Algorithm); actual.Id.Should().Be(fakeX509CertData.Id); actual.Created.Should().Be(fakeX509CertData.Created); @@ -130,17 +161,17 @@ public void Convert_WhenX509IdentityServerKeyData_ShouldConvertToSigningKey() } [Fact] - public void Convert_WhenDatProtected_ShouldConvertToSigningKey() + public void Convert_WhenDataProtected_ShouldConvertToSigningKey() { ECDsaSecurityKey fakeECDsaSecurityKey = new ECDsaSecurityKey(ECDsa.Create(FakeKeyData.EcDsaSecurityKey384)) { - KeyId = "Fake_EC384", + KeyId = "Fake_ES384", }; X509IdentityServerKeyData fakeX509CertData = new X509IdentityServerKeyData { Id = fakeECDsaSecurityKey.KeyId, Created = new DateTime(2026, 04, 25, 11, 20, 21, DateTimeKind.Utc), - Algorithm = "EC384", + Algorithm = "ES384", CertificateRawData = fakeECDsaSecurityKey.ToBase64Pfx(), }; var fakeX509Json = fakeX509CertData.ToExpectedJson(); @@ -161,7 +192,7 @@ public void Convert_WhenDatProtected_ShouldConvertToSigningKey() SigningKey actual = sut.Convert(testKeyMaterial); var expectedCert = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(fakeX509CertData.CertificateRawData), null); - SigningCredentials expectedCredentials = new SigningCredentials(new X509SecurityKey(expectedCert), fakeX509CertData.Algorithm); + SigningCredentials expectedCredentials = new SigningCredentials(new X509SecurityKey(expectedCert) { KeyId = fakeX509CertData.Id }, fakeX509CertData.Algorithm); actual.Id.Should().Be(fakeX509CertData.Id); actual.Created.Should().Be(fakeX509CertData.Created); diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/FakeKeyData.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/FakeKeyData.cs index 5f88af714..776cac4b7 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/FakeKeyData.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/FakeKeyData.cs @@ -12,7 +12,7 @@ namespace IdentityServer.UnitTests.Stores.Compatibility; public static class FakeKeyData { - public static RSAParameters RsaSecurityKey256 = new RSAParameters + public static RSAParameters RsaSecurityKey256 = new() { Modulus = Convert.FromBase64String("lhB1UxNfZTjo7y41Oj6YVOqk7Lr4kd4MEWkmEgS6OME+N7DPMlHQM0EUH5NswPKl/6gD8YW6vaMPtynhCO6X/tLnAfQXu0R7vPme8/5YsVhHcCzYYUZUUjCe2hCm0oeSdV+oVB9Q7N0RDbvI64upmKXs6WsmWQjR5gxVsWgtU4hnkBXgRMWSQZPTvAaGthGtT18e0xZPiRI3mi7++JonkgVpvln8lHubP4ov6OKOOmxW7yLaLFRyYJIKJpXl9G26QRV8VpwWDEV6klds2n9YNAuQJFCAD1qoFgk4p+JZm3yAFRHpZlQHL83E4LF2ub7bZwy/pSo948Ie0/S21uYiTw=="), Exponent = Convert.FromBase64String("AQAB"), @@ -24,7 +24,7 @@ public static class FakeKeyData InverseQ = Convert.FromBase64String("Rgz8DKhPbMDzBZJKnqqmNuqP5FEQVy9ixlHx1gjM1QzdXSOBxQM9hJ7xEVVeU72HQaGfONFfs+ywJRTTrGOYYFS0arQC7xSSKnQj84FGm/5M+geme5Libt2Tp2mNxcbUrxJBTHxoLAXvGfjlu7pL5xmYC/GJIOXiN8KoAP8O8i0=") }; - public static ECParameters EcDsaSecurityKey256 = new ECParameters + public static ECParameters EcDsaSecurityKey256 = new() { Curve = ECCurve.NamedCurves.nistP256, Q = new ECPoint @@ -35,7 +35,7 @@ public static class FakeKeyData D = Convert.FromBase64String("6l0Qd9ZoV5gFj7mrKuzDJvLuaCOAoWiuSuWhJMTFuts=") }; - public static ECParameters EcDsaSecurityKey384 = new ECParameters + public static ECParameters EcDsaSecurityKey384 = new() { Curve = ECCurve.NamedCurves.nistP384, Q = new ECPoint @@ -46,7 +46,7 @@ public static class FakeKeyData D = Convert.FromBase64String("FTG9qoWhAOdRtzovZJxq+4ZerL3u1Ji7zF8QNRRcZjpMLT1pLTwsq8ipwhXUjNTE") }; - public static ECParameters EcDsaSecurityKey521 = new ECParameters + public static ECParameters EcDsaSecurityKey521 = new() { Curve = ECCurve.NamedCurves.nistP521, Q = new ECPoint @@ -59,14 +59,20 @@ public static class FakeKeyData public static string ToBase64Pfx(this RsaSecurityKey rsaKey) { - using var rsa = RSA.Create(rsaKey.Parameters); - var request = new CertificateRequest($"CN={rsaKey.KeyId}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + using var rsa = RSA.Create(); + rsa.ImportParameters(rsaKey.Parameters); - using X509Certificate2 cert = request.CreateSelfSigned( + var request = new CertificateRequest( + $"CN={rsaKey.KeyId}", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + using var cert = request.CreateSelfSigned( DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1)); - return Convert.ToBase64String(cert.Export(X509ContentType.Pfx, null as string)); + return Convert.ToBase64String(cert.Export(X509ContentType.Pfx)); } public static string ToBase64Pfx(this ECDsaSecurityKey ecKey) diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs index d725d430c..2cb07c3bf 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs @@ -324,7 +324,7 @@ public async Task GetSigningCredentialsAsync_WhenProtected_AndSingleRsaKeyWrappe var actual = await sut.GetSigningCredentialsAsync(); var expectedCert = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(fakeX509CertData.CertificateRawData), null); - SigningCredentials expectedSigningCredentials = new SigningCredentials(new X509SecurityKey(expectedCert), fakeX509CertData.Algorithm); + SigningCredentials expectedSigningCredentials = new SigningCredentials(new X509SecurityKey(expectedCert) { KeyId = fakeX509CertData.Id }, fakeX509CertData.Algorithm); actual.Should().BeEquivalentTo(expectedSigningCredentials); } @@ -334,13 +334,13 @@ public async Task GetSigningCredentialsAsync_WhenProtected_AndSingleECDsaKeyWrap { var fakeECDsaSecurityKey = new ECDsaSecurityKey(ECDsa.Create(FakeKeyData.EcDsaSecurityKey384)) { - KeyId = "Fake_EC384", + KeyId = "Fake_ES384", }; var fakeX509CertData = new X509IdentityServerKeyData { Id = fakeECDsaSecurityKey.KeyId, Created = FakeNow.AddDays(-33), - Algorithm = "EC384", + Algorithm = "ES384", CertificateRawData = fakeECDsaSecurityKey.ToBase64Pfx(), }; var fakeX509Json = fakeX509CertData.ToExpectedJson(); @@ -363,7 +363,7 @@ public async Task GetSigningCredentialsAsync_WhenProtected_AndSingleECDsaKeyWrap var actual = await sut.GetSigningCredentialsAsync(); var expectedCert = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(fakeX509CertData.CertificateRawData), null); - SigningCredentials expectedSigningCredentials = new SigningCredentials(new X509SecurityKey(expectedCert), fakeX509CertData.Algorithm); + SigningCredentials expectedSigningCredentials = new SigningCredentials(new X509SecurityKey(expectedCert) { KeyId = fakeX509CertData.Id }, fakeX509CertData.Algorithm); actual.Should().BeEquivalentTo(expectedSigningCredentials); } From 38afbd8d195a4f32b7b86cc6fb979e0cb8c58fb1 Mon Sep 17 00:00:00 2001 From: James Britton Date: Fri, 5 Jun 2026 15:17:45 +0100 Subject: [PATCH 4/6] docs: updated documentation for compatibility store to talk about MaxLifetime setting --- docs/migrating/from_duende.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/migrating/from_duende.rst b/docs/migrating/from_duende.rst index 09866cacd..6a9f7b64d 100644 --- a/docs/migrating/from_duende.rst +++ b/docs/migrating/from_duende.rst @@ -102,7 +102,7 @@ Migration Steps .. _refReadOnlyKeyStore: - If you were using automatic key management, you will now need to register read-only key store or add a signing key explicitly: + If you were using automatic key management, you will now need to register the read-only key store and/or add a signing key(s) explicitly: .. code-block:: c# @@ -113,6 +113,12 @@ Migration Steps // Explicit key registration builder.Services.AddIdentityServer() .AddSigningCredential(certificate); + + .. warning:: + If configuring the read-only key store, all keys created within a 90 days will be considered valid, and the newest + key within this timespan will be registered as a signing key. The extension method used to register the + compatibility store allows you to configure this ``.AddCompatibilityKeyStores(opt => opt.MaxLifetime = TimeSpan.FromDays(120))``. + 5. **Migrate the database schema (if applicable)** From e213293d26b1a945d7f1a8a5b0c69524867c4bc4 Mon Sep 17 00:00:00 2001 From: James Britton Date: Fri, 5 Jun 2026 15:26:27 +0100 Subject: [PATCH 5/6] pr: correcting issues pointed out in PR --- .../Options/CompatibilityKeyStoreOptions.cs | 3 ++ .../src/DataProtectionConstants.cs | 4 +- .../Compatibility/IdentityServerKeyStore.cs | 5 +- .../IdentityServerSigningCredentialStore.cs | 2 +- .../IdentityServerValidationKeysStore.cs | 2 +- .../Discovery/FakeIdentityServerKeyStore.cs | 4 +- ...entityServerSigningCredentialStoreTests.cs | 12 ++--- .../IdentityServerValidationKeysStoreTests.cs | 46 +++++++++---------- 8 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/Open.IdentityServer/src/Configuration/DependencyInjection/Options/CompatibilityKeyStoreOptions.cs b/src/Open.IdentityServer/src/Configuration/DependencyInjection/Options/CompatibilityKeyStoreOptions.cs index 4675bd375..9bbfeb23c 100644 --- a/src/Open.IdentityServer/src/Configuration/DependencyInjection/Options/CompatibilityKeyStoreOptions.cs +++ b/src/Open.IdentityServer/src/Configuration/DependencyInjection/Options/CompatibilityKeyStoreOptions.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + using System; namespace Open.IdentityServer.Configuration; diff --git a/src/Open.IdentityServer/src/DataProtectionConstants.cs b/src/Open.IdentityServer/src/DataProtectionConstants.cs index 9f9433fde..9e0485dcf 100644 --- a/src/Open.IdentityServer/src/DataProtectionConstants.cs +++ b/src/Open.IdentityServer/src/DataProtectionConstants.cs @@ -6,8 +6,8 @@ namespace Open.IdentityServer; /// /// Constants for DataProtection in Open.IdentityServer. /// -public abstract class DataProtectionConstants +public static class DataProtectionConstants { - /// Purposed used when creating key material data protector + /// Purpose used when creating key material data protector. public const string KeyProtectorPurpose = "DataProtectionKeyProtector"; } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerKeyStore.cs b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerKeyStore.cs index 302e3a6e5..c0540403c 100644 --- a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerKeyStore.cs +++ b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerKeyStore.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + using System; using System.Collections.Generic; using System.Linq; @@ -29,7 +32,7 @@ public abstract class IdentityServerSigningKeyStore( /// Get all signing keys from the store, filtering based on the compatibility key store configuration and use /// /// - protected IEnumerable GetKeysAsync() + protected IEnumerable GetKeys() { return identityServerKeyStore.GetKeys() .Where(x => x.Use == "signing") diff --git a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerSigningCredentialStore.cs b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerSigningCredentialStore.cs index 5bc6cd422..253bdbb71 100644 --- a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerSigningCredentialStore.cs +++ b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerSigningCredentialStore.cs @@ -33,7 +33,7 @@ CompatibilityKeyStoreOptions options /// a key to be used for signing public async Task GetSigningCredentialsAsync() { - return GetKeysAsync() + return GetKeys() .Select(x => x.Credentials) .FirstOrDefault(); } diff --git a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerValidationKeysStore.cs b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerValidationKeysStore.cs index 53cb871b8..0be098739 100644 --- a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerValidationKeysStore.cs +++ b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerValidationKeysStore.cs @@ -34,7 +34,7 @@ CompatibilityKeyStoreOptions options /// list of validation keys info public async Task> GetValidationKeysAsync() { - return GetKeysAsync() + return GetKeys() .Select(x => new SecurityKeyInfo { Key = x.Credentials.Key, SigningAlgorithm = x.Credentials.Algorithm }) .ToList(); } diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/FakeIdentityServerKeyStore.cs b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/FakeIdentityServerKeyStore.cs index fa7fb80e0..75651a53d 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/FakeIdentityServerKeyStore.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/FakeIdentityServerKeyStore.cs @@ -7,7 +7,7 @@ namespace IdentityServer.IntegrationTests.Endpoints.Discovery; public class FakeIdentityServerKeyStore: IIdentityServerKeyStore { - public static DateTime FakeNow = new DateTime(2026, 02, 01, 12, 00, 00, DateTimeKind.Utc); + public static readonly DateTime FakeNow = new(2026, 02, 01, 12, 00, 00, DateTimeKind.Utc); public IEnumerable GetKeys() { @@ -18,7 +18,7 @@ public IEnumerable GetKeys() new IdentityServerKeyMaterial { Id = "FakeRSA_ES256", Version = 1, Use = "signing", DataProtected = false, Algorithm = "ES256", Data = FakeES256KeyData }, new IdentityServerKeyMaterial { Id = "FakeRSA_ES384", Version = 1, Use = "signing", DataProtected = false, Algorithm = "ES384", Data = FakeES384KeyData }, new IdentityServerKeyMaterial { Id = "FakeRSA_ES521", Version = 1, Use = "signing", DataProtected = false, Algorithm = "ES521", Data = FakeES521KeyData }, - new IdentityServerKeyMaterial { Id = "FakeRSA_PS512_Expired", Version = 1, Use = "signing", DataProtected = false, Algorithm = "PS256", Data = FakePS512KeyData }, + new IdentityServerKeyMaterial { Id = "FakeRSA_PS512_Expired", Version = 1, Use = "signing", DataProtected = false, Algorithm = "PS512", Data = FakePS512KeyData }, ]; } diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs index 2cb07c3bf..464480d1a 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs @@ -29,13 +29,13 @@ public class IdentityServerSigningCredentialStoreTests private readonly FakeTimeProvider timeProvider = new(); private readonly CompatibilityKeyStoreOptions fakeOptions = new(); - private DateTime FakeNow = new(2026, 05, 01, 12, 00, 00, DateTimeKind.Utc); + private readonly DateTime fakeNow = new(2026, 05, 01, 12, 00, 00, DateTimeKind.Utc); private List fakeKeyMaterials = []; public IdentityServerSigningCredentialStoreTests() { - timeProvider.SetUtcNow(FakeNow); + timeProvider.SetUtcNow(fakeNow); Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) @@ -134,7 +134,7 @@ public async Task GetSigningCredentialsAsync_ShouldFilterOutKeysThatExceedMaxLif var fakeRsaKey = new RsaIdentityServerKeyData { Id = "Fake_RS256", - Created = FakeNow.AddDays(-130), + Created = fakeNow.AddDays(-130), Algorithm = "RS256", Parameters = FakeKeyData.RsaSecurityKey256, }; @@ -142,7 +142,7 @@ public async Task GetSigningCredentialsAsync_ShouldFilterOutKeysThatExceedMaxLif var fakeEcKey = new EcIdentityServerKeyData { Id = "Fake_ES256", - Created = FakeNow.AddDays(-99), + Created = fakeNow.AddDays(-99), Algorithm = "ES256", D = FakeKeyData.EcDsaSecurityKey256.D, Q = FakeKeyData.EcDsaSecurityKey256.Q, @@ -300,7 +300,7 @@ public async Task GetSigningCredentialsAsync_WhenProtected_AndSingleRsaKeyWrappe var fakeX509CertData = new X509IdentityServerKeyData { Id = fakeRsaSecurityKey.KeyId, - Created = FakeNow.AddDays(-33), + Created = fakeNow.AddDays(-33), Algorithm = "RS256", CertificateRawData = fakeRsaSecurityKey.ToBase64Pfx(), }; @@ -339,7 +339,7 @@ public async Task GetSigningCredentialsAsync_WhenProtected_AndSingleECDsaKeyWrap var fakeX509CertData = new X509IdentityServerKeyData { Id = fakeECDsaSecurityKey.KeyId, - Created = FakeNow.AddDays(-33), + Created = fakeNow.AddDays(-33), Algorithm = "ES384", CertificateRawData = fakeECDsaSecurityKey.ToBase64Pfx(), }; diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs index a9cfd713d..fd386d5b8 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs @@ -20,24 +20,24 @@ namespace IdentityServer.UnitTests.Stores.Compatibility; public class IdentityServerValidationKeysStoreTests { - private readonly IIdentityServerKeyStore _identityServerKeyStore = Mock.Of(); + private readonly IIdentityServerKeyStore identityServerKeyStore = Mock.Of(); private readonly IDataProtectionProvider dataProtectionProvider = Mock.Of(); private IDataProtector dataProtector = Mock.Of(); - private readonly DataProtectedIdentityServerKeyMaterialConverter _dataProtectedIdentityServerKeyMaterialConverter; + private readonly DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter; private readonly FakeTimeProvider timeProvider = new(); private readonly CompatibilityKeyStoreOptions fakeOptions = new(); - private DateTime FakeNow = new DateTime(2026, 05, 01, 12, 00, 00, DateTimeKind.Utc); + private readonly DateTime fakeNow = new(2026, 05, 01, 12, 00, 00, DateTimeKind.Utc); private List fakeKeyMaterials = []; public IdentityServerValidationKeysStoreTests() { - timeProvider.SetUtcNow(FakeNow); + timeProvider.SetUtcNow(fakeNow); - _dataProtectedIdentityServerKeyMaterialConverter = new DataProtectedIdentityServerKeyMaterialConverter(dataProtectionProvider); + dataProtectedIdentityServerKeyMaterialConverter = new DataProtectedIdentityServerKeyMaterialConverter(dataProtectionProvider); - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns(fakeKeyMaterials); @@ -46,7 +46,7 @@ public IdentityServerValidationKeysStoreTests() .Returns(dataProtector); } - private IdentityServerValidationKeysStore CreateSut() => new(_identityServerKeyStore, _dataProtectedIdentityServerKeyMaterialConverter, timeProvider, fakeOptions); + private IdentityServerValidationKeysStore CreateSut() => new(identityServerKeyStore, dataProtectedIdentityServerKeyMaterialConverter, timeProvider, fakeOptions); [Fact] public async Task GetValidationKeysAsync_ShouldReturnCollectionOfSecurityKeyInfo() @@ -54,7 +54,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionOfSecurityKeyInfo var fakeRsaKey0 = new RsaIdentityServerKeyData { Id = "Fake_RS256_0", - Created = FakeNow.AddDays(-20), + Created = fakeNow.AddDays(-20), Algorithm = "RS256", Parameters = FakeKeyData.RsaSecurityKey256, }; @@ -62,7 +62,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionOfSecurityKeyInfo var fakeRsaKey1 = new RsaIdentityServerKeyData { Id = "Fake_RS256_1", - Created = FakeNow.AddDays(-10), + Created = fakeNow.AddDays(-10), Algorithm = "RS256", Parameters = FakeKeyData.RsaSecurityKey256, }; @@ -70,7 +70,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionOfSecurityKeyInfo var fakeEcKey0 = new EcIdentityServerKeyData { Id = "Fake_ES384_0", - Created = FakeNow.AddDays(-30), + Created = fakeNow.AddDays(-30), Algorithm = "ES384", D = FakeKeyData.EcDsaSecurityKey384.D, Q = FakeKeyData.EcDsaSecurityKey384.Q, @@ -79,7 +79,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionOfSecurityKeyInfo var fakeEcKey1 = new EcIdentityServerKeyData { Id = "Fake_ES521_0", - Created = FakeNow.AddDays(-45), + Created = fakeNow.AddDays(-45), Algorithm = "ES521", D = FakeKeyData.EcDsaSecurityKey521.D, Q = FakeKeyData.EcDsaSecurityKey521.Q, @@ -92,7 +92,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionOfSecurityKeyInfo new IdentityServerKeyMaterial { Id = fakeEcKey1.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeEcKey1.Algorithm, Data = fakeEcKey1.ToExpectedJson() }, ]; - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns(fakeKeyMaterials); @@ -126,7 +126,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A var fakeRsaKey0 = new RsaIdentityServerKeyData { Id = "Fake_RS256_0", - Created = FakeNow.AddDays(-10), + Created = fakeNow.AddDays(-10), Algorithm = "RS256", Parameters = FakeKeyData.RsaSecurityKey256, }; @@ -134,7 +134,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A var fakeRsaKey1 = new RsaIdentityServerKeyData { Id = "Fake_RS256_1", - Created = FakeNow.AddDays(-30), + Created = fakeNow.AddDays(-30), Algorithm = "RS256", Parameters = FakeKeyData.RsaSecurityKey256, }; @@ -142,7 +142,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A var fakeEcKey0 = new EcIdentityServerKeyData { Id = "Fake_ES256_0", - Created = FakeNow.AddHours(-12), + Created = fakeNow.AddHours(-12), Algorithm = "ES256", D = FakeKeyData.EcDsaSecurityKey256.D, Q = FakeKeyData.EcDsaSecurityKey256.Q, @@ -151,7 +151,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A var fakeEcKey1 = new EcIdentityServerKeyData { Id = "Fake_ES521_0", - Created = FakeNow.AddDays(-20), + Created = fakeNow.AddDays(-20), Algorithm = "ES521", D = FakeKeyData.EcDsaSecurityKey521.D, Q = FakeKeyData.EcDsaSecurityKey521.Q, @@ -164,7 +164,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A new IdentityServerKeyMaterial { Id = fakeEcKey1.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeEcKey1.Algorithm, Data = fakeEcKey1.ToExpectedJson() }, ]; - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns(fakeKeyMaterials); @@ -191,7 +191,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A var fakeRsaKey0 = new RsaIdentityServerKeyData { Id = "Fake_RS256_0", - Created = FakeNow.AddDays(-30), + Created = fakeNow.AddDays(-30), Algorithm = "RS256", Parameters = FakeKeyData.RsaSecurityKey256, }; @@ -199,7 +199,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A var fakeRsaKey1 = new RsaIdentityServerKeyData { Id = "Fake_RS256_1", - Created = FakeNow.AddHours(-12), + Created = fakeNow.AddHours(-12), Algorithm = "RS256", Parameters = FakeKeyData.RsaSecurityKey256, }; @@ -207,7 +207,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A var fakeEcKey0 = new EcIdentityServerKeyData { Id = "Fake_ES256_0", - Created = FakeNow.AddDays(-100), + Created = fakeNow.AddDays(-100), Algorithm = "ES256", D = FakeKeyData.EcDsaSecurityKey256.D, Q = FakeKeyData.EcDsaSecurityKey256.Q, @@ -216,7 +216,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A var fakeEcKey1 = new EcIdentityServerKeyData { Id = "Fake_ES521_0", - Created = FakeNow.AddDays(-10), + Created = fakeNow.AddDays(-10), Algorithm = "ES521", D = FakeKeyData.EcDsaSecurityKey521.D, Q = FakeKeyData.EcDsaSecurityKey521.Q, @@ -229,7 +229,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A new IdentityServerKeyMaterial { Id = fakeEcKey1.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeEcKey1.Algorithm, Data = fakeEcKey1.ToExpectedJson() }, ]; - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns(fakeKeyMaterials); @@ -253,7 +253,7 @@ public async Task GetValidationKeysAsync_ShouldReturnCollectionSecurityKeyInfo_A [Fact] public async Task GetValidationKeysAsync_WhenTableIsEmpty_ShouldReturnEmptyList() { - Mock.Get(_identityServerKeyStore) + Mock.Get(identityServerKeyStore) .Setup(x => x.GetKeys()) .Returns([]); From a169304d44ae5407c180eca2a008ce3159e927b4 Mon Sep 17 00:00:00 2001 From: James Britton Date: Mon, 8 Jun 2026 15:34:13 +0100 Subject: [PATCH 6/6] pr: correcting missing copyright header and bug with created time comparison for filtering keys on age --- .../Compatibility/IdentityServerKeyStore.cs | 6 +- .../Discovery/FakeIdentityServerKeyStore.cs | 3 + .../LocalTimeZoneInfoMocker.cs | 21 +++++ ...entityServerSigningCredentialStoreTests.cs | 34 ++++++++ .../IdentityServerValidationKeysStoreTests.cs | 78 +++++++++++++++++++ 5 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/LocalTimeZoneInfoMocker.cs diff --git a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerKeyStore.cs b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerKeyStore.cs index c0540403c..b5ba1c614 100644 --- a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerKeyStore.cs +++ b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerKeyStore.cs @@ -34,10 +34,12 @@ public abstract class IdentityServerSigningKeyStore( /// protected IEnumerable GetKeys() { - return identityServerKeyStore.GetKeys() + var results = identityServerKeyStore.GetKeys() .Where(x => x.Use == "signing") .Select(dataProtectedIdentityServerKeyMaterialConverter.Convert) - .Where(x => x.Created.Add(options.MaxLifetime) > timeProvider.GetUtcNow()) + .Where(x => x.Created.Add(options.MaxLifetime) > timeProvider.GetUtcNow().UtcDateTime) .OrderByDescending(x => x.Created); + + return results; } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/FakeIdentityServerKeyStore.cs b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/FakeIdentityServerKeyStore.cs index 75651a53d..d75543404 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/FakeIdentityServerKeyStore.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.IntegrationTests/Endpoints/Discovery/FakeIdentityServerKeyStore.cs @@ -1,3 +1,6 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + using System; using System.Collections.Generic; using Open.IdentityServer.Models; diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/LocalTimeZoneInfoMocker.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/LocalTimeZoneInfoMocker.cs new file mode 100644 index 000000000..b881e3cb9 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/LocalTimeZoneInfoMocker.cs @@ -0,0 +1,21 @@ +using System; +using System.Reflection; + +namespace Open.IdentityServer.UnitTests; + +public class LocalTimeZoneInfoMocker: IDisposable +{ + public LocalTimeZoneInfoMocker(TimeZoneInfo mockTimeZoneInfo) + { + var info = typeof(TimeZoneInfo).GetField("s_cachedData", BindingFlags.NonPublic | BindingFlags.Static); + var cachedData = info?.GetValue(null); + var field = cachedData?.GetType().GetField("_localTimeZone", + BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Instance); + field?.SetValue(cachedData, mockTimeZoneInfo); + } + + public void Dispose() + { + TimeZoneInfo.ClearCachedData(); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs index 464480d1a..f70e48ccb 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs @@ -16,6 +16,7 @@ using Open.IdentityServer.DataProtection; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; +using Open.IdentityServer.UnitTests; using Xunit; namespace IdentityServer.UnitTests.Stores.Compatibility; @@ -50,6 +51,39 @@ public IdentityServerSigningCredentialStoreTests() private IdentityServerSigningCredentialStore CreateSut() => new(identityServerKeyStore, dataProtectedIdentityServerKeyMaterialConverter, timeProvider, fakeOptions); + [Fact] + public async Task GetSigningCredentialsAsync_WhenUnspecifiedDateTime_ShouldTreatAsUtcTime() + { + //Ensuring timezone info is the same across environments + using var mockedTimezone = new LocalTimeZoneInfoMocker(TimeZoneInfo.FindSystemTimeZoneById("China Standard Time")); + + var testCreated = fakeNow.AddDays(-90).AddHours(1); + + var fakeRsaKey = new RsaIdentityServerKeyData + { + Id = "Fake_RS256", + Created = new DateTime(testCreated.Year, testCreated.Month, testCreated.Day, testCreated.Hour, testCreated.Minute, testCreated.Second, DateTimeKind.Unspecified), //Subtracts in total 89 days, 23 hours + Algorithm = "RS256", + Parameters = FakeKeyData.RsaSecurityKey256, + }; + + fakeKeyMaterials = [ + new IdentityServerKeyMaterial { Id = fakeRsaKey.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeRsaKey.Algorithm, Data = fakeRsaKey.ToExpectedJson() }, + ]; + + Mock.Get(identityServerKeyStore) + .Setup(x => x.GetKeys()) + .Returns(fakeKeyMaterials); + + var sut = CreateSut(); + var actual = await sut.GetSigningCredentialsAsync(); + + SigningCredentials expectedSigningCredentials = new SigningCredentials(new RsaSecurityKey(fakeRsaKey.Parameters) { KeyId = fakeRsaKey.Id }, fakeRsaKey.Algorithm); + + actual.Should().BeEquivalentTo(expectedSigningCredentials); + + } + [Fact] public async Task GetSigningCredentialsAsync_WhenDataUnprotected_AndLatestRsaKey_ShouldReturnSigningCredentialsRepresenting() { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs index fd386d5b8..c5238047c 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs @@ -14,6 +14,7 @@ using Open.IdentityServer.DataProtection; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; +using Open.IdentityServer.UnitTests; using Xunit; namespace IdentityServer.UnitTests.Stores.Compatibility; @@ -47,6 +48,83 @@ public IdentityServerValidationKeysStoreTests() } private IdentityServerValidationKeysStore CreateSut() => new(identityServerKeyStore, dataProtectedIdentityServerKeyMaterialConverter, timeProvider, fakeOptions); + + [Fact] + public async Task GetValidationKeysAsync_WhenUnspecifiedDateTime_ShouldTreatAsUtcTime() + { + //Ensuring timezone info is the same across environments + using var mockedTimezone = new LocalTimeZoneInfoMocker(TimeZoneInfo.FindSystemTimeZoneById("China Standard Time")); + + var fakeRsaKey0Created = fakeNow.AddDays(-90).AddHours(1); + var fakeRsaKey0 = new RsaIdentityServerKeyData + { + Id = "Fake_RS256_0", + Created = new DateTime(fakeRsaKey0Created.Year, fakeRsaKey0Created.Month, fakeRsaKey0Created.Day, fakeRsaKey0Created.Hour, fakeRsaKey0Created.Minute, fakeRsaKey0Created.Second, DateTimeKind.Unspecified), //Subtracts in total 89 days, 23 hours + Algorithm = "RS256", + Parameters = FakeKeyData.RsaSecurityKey256, + }; + + var fakeRsaKey1 = new RsaIdentityServerKeyData + { + Id = "Fake_RS256_1", + Created = fakeNow.AddDays(-10), + Algorithm = "RS256", + Parameters = FakeKeyData.RsaSecurityKey256, + }; + + var fakeEcKey0Created = fakeNow.AddDays(-90).AddHours(2); + var fakeEcKey0 = new EcIdentityServerKeyData + { + Id = "Fake_ES384_0", + Created = new DateTime(fakeEcKey0Created.Year, fakeEcKey0Created.Month, fakeEcKey0Created.Day, fakeEcKey0Created.Hour, fakeEcKey0Created.Minute, fakeEcKey0Created.Second, DateTimeKind.Unspecified), //Subtracts in total 89 days, 22 hours + Algorithm = "ES384", + D = FakeKeyData.EcDsaSecurityKey384.D, + Q = FakeKeyData.EcDsaSecurityKey384.Q, + }; + + var fakeEcKey1 = new EcIdentityServerKeyData + { + Id = "Fake_ES521_0", + Created = fakeNow.AddDays(-45), + Algorithm = "ES521", + D = FakeKeyData.EcDsaSecurityKey521.D, + Q = FakeKeyData.EcDsaSecurityKey521.Q, + }; + + fakeKeyMaterials = [ + new IdentityServerKeyMaterial { Id = fakeRsaKey0.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeRsaKey0.Algorithm, Data = fakeRsaKey0.ToExpectedJson() }, + new IdentityServerKeyMaterial { Id = fakeRsaKey1.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeRsaKey1.Algorithm, Data = fakeRsaKey1.ToExpectedJson() }, + new IdentityServerKeyMaterial { Id = fakeEcKey0.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeEcKey0.Algorithm, Data = fakeEcKey0.ToExpectedJson() }, + new IdentityServerKeyMaterial { Id = fakeEcKey1.Id, Version = 1, Use = "signing", DataProtected = false, Algorithm = fakeEcKey1.Algorithm, Data = fakeEcKey1.ToExpectedJson() }, + ]; + + Mock.Get(identityServerKeyStore) + .Setup(x => x.GetKeys()) + .Returns(fakeKeyMaterials); + + var sut = CreateSut(); + var actual = await sut.GetValidationKeysAsync(); + + + ECDsa expectedEcDsa0 = ECDsa.Create(new ECParameters + { + Curve = CryptoHelper.GetCurveFromCrvValue("P-384"), D = fakeEcKey0.D, Q = fakeEcKey0.Q, + }); + + ECDsa expectedEcDsa1 = ECDsa.Create(new ECParameters + { + Curve = CryptoHelper.GetCurveFromCrvValue("P-521"), D = fakeEcKey1.D, Q = fakeEcKey1.Q, + }); + + IEnumerable expectedCredentials = [ + new() { Key = new RsaSecurityKey(fakeRsaKey0.Parameters) { KeyId = fakeRsaKey0.Id }, SigningAlgorithm = fakeRsaKey0.Algorithm }, + new() { Key = new RsaSecurityKey(fakeRsaKey1.Parameters) { KeyId = fakeRsaKey1.Id }, SigningAlgorithm = fakeRsaKey1.Algorithm }, + new() { Key = new ECDsaSecurityKey(expectedEcDsa0) { KeyId = fakeEcKey0.Id }, SigningAlgorithm = fakeEcKey0.Algorithm }, + new() { Key = new ECDsaSecurityKey(expectedEcDsa1) { KeyId = fakeEcKey1.Id }, SigningAlgorithm = fakeEcKey1.Algorithm }, + ]; + + actual.Should().BeEquivalentTo(expectedCredentials); + } [Fact] public async Task GetValidationKeysAsync_ShouldReturnCollectionOfSecurityKeyInfo()