// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.IO; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Testing.xunit; using Xunit; namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration { public class SigningKeysLoaderTests { // We need to cast the underlying int value of the EphemeralKeySet to X509KeyStorageFlags // due to the fact that is not part of .NET Standard. This value is only used with non-windows // platforms (all .NET Core) for which the value is defined on the underlying platform. private const X509KeyStorageFlags UnsafeEphemeralKeySet = (X509KeyStorageFlags)32; private static readonly X509KeyStorageFlags DefaultFlags = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? UnsafeEphemeralKeySet : (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? X509KeyStorageFlags.PersistKeySet : X509KeyStorageFlags.DefaultKeySet); [Fact] public void LoadFromFile_ThrowsIfFileDoesNotExist() { // Arrange, Act & Assert var exception = Assert.Throws(() => SigningKeysLoader.LoadFromFile("./nonexisting.pfx", "", DefaultFlags)); Assert.Equal($"There was an error loading the certificate. The file './nonexisting.pfx' was not found.", exception.Message); } [Fact] public void LoadFromFile_ThrowsIfPasswordIsNull() { // Arrange, Act & Assert var exception = Assert.Throws(() => SigningKeysLoader.LoadFromFile("test.pfx", null, DefaultFlags)); Assert.Equal("There was an error loading the certificate. No password was provided.", exception.Message); } [Fact] public void LoadFromFile_ThrowsIfPasswordIsIncorrect() { // Arrange, Act & Assert var exception = Assert.Throws(() => SigningKeysLoader.LoadFromFile("test.pfx", "incorrect", DefaultFlags)); Assert.Equal( $"There was an error loading the certificate. Either the password is incorrect or the process does not have permisions to store the key in the Keyset '{DefaultFlags}'", exception.Message); } [Fact] public static void LoadFromStoreCert_ThrowsIfThereIsNoCertificateAvailable() { // Arrange var time = new DateTimeOffset(2018, 09, 25, 12, 0, 0, TimeSpan.Zero); // Act & Assert var exception = Assert.Throws(() => SigningKeysLoader.LoadFromStoreCert("Invalid", "My", StoreLocation.CurrentUser, time)); Assert.Equal("Couldn't find a valid certificate with subject 'Invalid' on the 'CurrentUser\\My'", exception.Message); } [Fact] public static void LoadFromStoreCert_SkipsCertificatesNotYetValid() { try { SetupCertificates("./current.pfx", "./future.pfx"); // Arrange var time = new DateTimeOffset(2018, 10, 29, 12, 0, 0, TimeSpan.Zero); // Act var certificate = SigningKeysLoader.LoadFromStoreCert("CN=SigningKeysLoaderTest", "My", StoreLocation.CurrentUser, time); // Assert Assert.NotNull(certificate); Assert.Equal("C54CD513088C23EC2AFD256874CC6C0F81EA9D5E", certificate.Thumbprint); } finally { CleanupCertificates(); } } [Fact] public static void LoadFromStoreCert_PrefersCertificatesCloserToExpirationDate() { try { SetupCertificates("./current.pfx", "./future.pfx"); // Arrange var time = new DateTimeOffset(2020, 10, 29, 12, 0, 0, TimeSpan.Zero); // Act var certificate = SigningKeysLoader.LoadFromStoreCert("CN=SigningKeysLoaderTest", "My", StoreLocation.CurrentUser, time); // Assert Assert.NotNull(certificate); Assert.Equal("C54CD513088C23EC2AFD256874CC6C0F81EA9D5E", certificate.Thumbprint); } finally { CleanupCertificates(); } } [Fact] public static void LoadFromStoreCert_SkipsExpiredCertificates() { try { SetupCertificates("./expired.pfx", "./current.pfx", "./future.pfx"); // Arrange var time = new DateTimeOffset(2024, 01, 01, 12, 0, 0, TimeSpan.Zero); // Act var certificate = SigningKeysLoader.LoadFromStoreCert("CN=SigningKeysLoaderTest", "My", StoreLocation.CurrentUser, time); // Assert Assert.NotNull(certificate); Assert.Equal("35840DD366107B89D2885A6B4F42CCBBAE6BA8E3", certificate.Thumbprint); } finally { CleanupCertificates(); } } [Fact] public static void LoadDevelopment_ThrowsIfKeyDoesNotExist() { // Act & Assert var exception = Assert.Throws(() => SigningKeysLoader.LoadDevelopment("c:/inexistent.json", createIfMissing: false)); Assert.Equal("Couldn't find the file 'c:/inexistent.json' and creation of a development key was not requested.", exception.Message); } [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.CLR)] public static void LoadDevelopment_CreatesKeyIfItDoesNotExist() { // Arrange var path = "./tempkeyfolder/tempkey.json"; if (File.Exists(path)) { File.Delete(path); } // Act var key = SigningKeysLoader.LoadDevelopment(path, createIfMissing: true); // Assert Assert.NotNull(key); Assert.True(File.Exists(path)); } [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.CLR)] public static void LoadDevelopment_ReusesKeyIfExists() { // Arrange var path = "./tempkeyfolder/existing.json"; if (File.Exists(path)) { File.Delete(path); } var existingKey = SigningKeysLoader.LoadDevelopment(path, createIfMissing: true); var existingParameters = existingKey.ExportParameters(includePrivateParameters: true); // Act var currentKey = SigningKeysLoader.LoadDevelopment(path, createIfMissing: true); var currentParameters = currentKey.ExportParameters(includePrivateParameters: true); // Assert Assert.NotNull(currentKey); Assert.Equal(existingParameters.P, currentParameters.P); Assert.Equal(existingParameters.Q, currentParameters.Q); } private static void CleanupCertificates() { using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) { store.Open(OpenFlags.ReadWrite); store.RemoveRange(store.Certificates.Find(X509FindType.FindBySubjectName, "CN=SigningKeysLoaderTest", validOnly: false)); store.Close(); } } private static void SetupCertificates(params string[] certificateFiles) { using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) { store.Open(OpenFlags.ReadWrite); foreach (var certificate in certificateFiles) { var cert = new X509Certificate2(certificate, "aspnetcore", DefaultFlags); if (!(store.Certificates.Find(X509FindType.FindByThumbprint, cert.Thumbprint, validOnly: false).Count > 0)) { store.Add(cert); } } store.Close(); } } } }