diff --git a/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj b/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj index 06e6fc1..e4a25e3 100644 --- a/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj +++ b/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj @@ -13,7 +13,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/AdvancedSystems.Security.Tests/Cryptography/CryptoRandomProviderTests.cs b/AdvancedSystems.Security.Tests/Cryptography/CryptoRandomProviderTests.cs index 72aaef3..d3fe3ec 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/CryptoRandomProviderTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/CryptoRandomProviderTests.cs @@ -7,10 +7,17 @@ namespace AdvancedSystems.Security.Tests.Cryptography; -public class CryptoRandomProviderTests +/// +/// Tests the public methods in . +/// +public sealed class CryptoRandomProviderTests { #region Tests + /// + /// Tests that returns an + /// array of non-zero bytes with the correct size. + /// [Fact] public void TestGetBytes() { @@ -25,6 +32,9 @@ public void TestGetBytes() Assert.All(buffer.ToArray(), b => Assert.InRange(b, byte.MinValue, byte.MaxValue)); } + /// + /// Tests heuristically that returns an integer. + /// [Fact] public void TestGetInt32() { @@ -42,6 +52,9 @@ public void TestGetInt32() Assert.All(randomNumbers, x => Assert.InRange(x, int.MinValue, int.MaxValue)); } + /// + /// Tests heuristically that returns an integer. + /// [Fact] public void TestGetInt32_MinMax() { @@ -61,6 +74,10 @@ public void TestGetInt32_MinMax() Assert.All(randomNumbers, x => Assert.InRange(x, min, max - 1)); } + /// + /// Tests that changes the order + /// of elements in an array. + /// [Fact] public void TestShuffle() { @@ -75,6 +92,10 @@ public void TestShuffle() Assert.NotEqual(array1, array2); } + /// + /// Tests that elements returned by + /// are elements of the original collection. + /// [Fact] public void TestChoice() { diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs index 90f2f4b..1fc11c6 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs @@ -8,10 +8,25 @@ namespace AdvancedSystems.Security.Tests.Cryptography; -public class HashTests +/// +/// Tests the public methods in . +/// +public sealed class HashTests { #region Tests + /// + /// Tests that the computed hash returns a well-formatted string. + /// + /// + /// The input to compute the hash code for. + /// + /// + /// The expected result. + /// + /// + /// The formatting to use. + /// [Theory] [InlineData("Hello, World!", "65a8e27d8879283831b664bd8b7f0ad4", Format.Hex)] [InlineData("Hello, World!", "ZajifYh5KDgxtmS9i38K1A==", Format.Base64)] @@ -31,6 +46,18 @@ public void TestMd5Hash(string input, string expected, Format format) Assert.Equal(expected, md5); } + /// + /// Tests that the computed hash returns a well-formatted string. + /// + /// + /// The input to compute the hash code for. + /// + /// + /// The expected result. + /// + /// + /// The formatting to use. + /// [Theory] [InlineData("Hello, World!", "0a0a9f2a6772942557ab5355d76af442f8f65e01", Format.Hex)] [InlineData("Hello, World!", "CgqfKmdylCVXq1NV12r0Qvj2XgE=", Format.Base64)] @@ -50,6 +77,18 @@ public void TestSHA1Hash(string input, string expected, Format format) Assert.Equal(expected, sha1); } + /// + /// Tests that the computed hash returns a well-formatted string. + /// + /// + /// The input to compute the hash code for. + /// + /// + /// The expected result. + /// + /// + /// The formatting to use. + /// [Theory] [InlineData("Hello, World!", "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f", Format.Hex)] [InlineData("Hello, World!", "3/1gIbsr1bCvZ2KQgJ7DpTGR3YHH9wpLKGiKNiGCmG8=", Format.Base64)] @@ -69,6 +108,18 @@ public void TestSHA256Hash(string input, string expected, Format format) Assert.Equal(expected, sha256); } + /// + /// Tests that the computed hash returns a well-formatted string. + /// + /// + /// The input to compute the hash code for. + /// + /// + /// The expected result. + /// + /// + /// The formatting to use. + /// [Theory] [InlineData("Hello, World!", "5485cc9b3365b4305dfb4e8337e0a598a574f8242bf17289e0dd6c20a3cd44a089de16ab4ab308f63e44b1170eb5f515", Format.Hex)] [InlineData("Hello, World!", "VIXMmzNltDBd+06DN+ClmKV0+CQr8XKJ4N1sIKPNRKCJ3harSrMI9j5EsRcOtfUV", Format.Base64)] @@ -88,6 +139,18 @@ public void TestSHA384Hash(string input, string expected, Format format) Assert.Equal(expected, sha384); } + /// + /// Tests that the computed hash returns a well-formatted string. + /// + /// + /// The input to compute the hash code for. + /// + /// + /// The expected result. + /// + /// + /// The formatting to use. + /// [Theory] [InlineData("Hello, World!", "374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6cc69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387", Format.Hex)] [InlineData("Hello, World!", "N015SpXNz9izWZMYX++bo2jxYNja9DLQi6nx7R5avmzGkpHg+i/gAGpSVw7xjBne9OYXwzzlLvCm5fvjGMsDhw==", Format.Base64)] diff --git a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..f20fcfa --- /dev/null +++ b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,250 @@ +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.DependencyInjection; +using AdvancedSystems.Security.Options; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +using Xunit; + +namespace AdvancedSystems.Security.Tests.DependencyInjection; + +/// +/// Tests the public methods in . +/// +public sealed class ServiceCollectionExtensionsTests +{ + #region AddCertificateService Tests + + /// + /// Tests that can be initialized through dependency injection from configuration options. + /// + [Fact] + public async Task TestAddCertificateService_FromOptions() + { + // Arrange + using var hostBuilder = await new HostBuilder() + .ConfigureWebHost(builder => builder + .UseTestServer() + .ConfigureServices(services => + { + services.AddCertificateService(options => + { + options.Thumbprint = "123456789"; + options.Store = new CertificateStoreOptions + { + Location = StoreLocation.CurrentUser, + Name = StoreName.My, + }; + }); + }) + .Configure(app => + { + + })) + .StartAsync(); + + // Act + var certificateService = hostBuilder.Services.GetService(); + var certificate = certificateService?.GetConfiguredCertificate(); + + // Assert + Assert.Multiple(() => + { + Assert.NotNull(certificateService); + Assert.Null(certificate); + }); + + await hostBuilder.StopAsync(); + } + + /// + /// Tests that can be initialized through dependency injection from configuration sections. + /// + [Fact] + public async Task TestAddCertificateService_FromAppSettings() + { + // Arrange + using var hostBuilder = await new HostBuilder() + .ConfigureWebHost(builder => builder + .UseTestServer() + .ConfigureAppConfiguration(config => + { + config.AddJsonFile("appsettings.json", optional: false); + }) + .ConfigureServices((context, services) => + { + services.AddCertificateService(context.Configuration.GetSection(Sections.CERTIFICATE)); + }) + .Configure(app => + { + + })) + .StartAsync(); + + // Act + var certificateService = hostBuilder.Services.GetService(); + var certificate = certificateService?.GetConfiguredCertificate(); + + // Assert + Assert.Multiple(() => + { + Assert.NotNull(certificateService); + Assert.Null(certificate); + }); + + await hostBuilder.StopAsync(); + } + + #endregion + + #region AddCertificateStore Tests + + /// + /// Tests that can be initialized through dependency injection from configuration options. + /// + [Fact] + public async Task TestAddCertificateStore_FromOptions() + { + // Arrange + using var hostBuilder = await new HostBuilder() + .ConfigureWebHost(builder => builder + .UseTestServer() + .ConfigureServices(services => + { + services.AddCertificateStore(options => + { + options.Name = StoreName.My; + options.Location = StoreLocation.CurrentUser; + }); + }) + .Configure(app => + { + + })) + .StartAsync(); + + // Act + var certificateStore = hostBuilder.Services.GetService(); + + // Assert + Assert.NotNull(certificateStore); + await hostBuilder.StopAsync(); + } + + /// + /// Tests that can be initialized through dependency injection from configuration sections. + /// + [Fact] + public async Task TestAddCertificateStore_FromAppSettings() + { + // Arrange + using var hostBuilder = await new HostBuilder() + .ConfigureWebHost(builder => builder + .UseTestServer() + .ConfigureAppConfiguration(config => + { + config.AddJsonFile("appsettings.json", optional: false); + }) + .ConfigureServices((context, services) => + { + services.AddCertificateStore(context.Configuration.GetSection($"{Sections.CERTIFICATE}:{Sections.STORE}")); + }) + .Configure(app => + { + + })) + .StartAsync(); + + // Act + var certificateStore = hostBuilder.Services.GetService(); + + // Assert + Assert.NotNull(certificateStore); + await hostBuilder.StopAsync(); + } + + #endregion + + #region AddCryptoRandomService Tests + + /// + /// Tests that can be initialized through dependency injection. + /// + [Fact] + public async Task TestAddCryptoRandomService() + { + // Arrange + using var hostBuilder = await new HostBuilder() + .ConfigureWebHost(builder => + { + builder.UseTestServer(); + builder.ConfigureServices(services => + { + services.AddCryptoRandomService(); + }); + builder.Configure(app => + { + + }); + }) + .StartAsync(); + + // Act + var cryptoRandomService = hostBuilder.Services.GetService(); + + // Assert + Assert.NotNull(cryptoRandomService); + await hostBuilder.StopAsync(); + } + + #endregion + + #region AddHashService Tests + + /// + /// Tests that can be initialized through dependency injection. + /// + [Fact] + public async Task TestAddHashService() + { + // Arrange + using var hostBuilder = await new HostBuilder() + .ConfigureWebHost(builder => builder + .UseTestServer() + .ConfigureServices(services => + { + services.AddHashService(); + }) + .Configure(app => + { + + })) + .StartAsync(); + + string input = "The quick brown fox jumps over the lazy dog"; + byte[] buffer = Encoding.UTF8.GetBytes(input); + + // Act + var hashService = hostBuilder.Services.GetService(); + string? sha256 = hashService?.GetSHA256Hash(buffer); + + // Assert + Assert.Multiple(() => + { + Assert.NotNull(hashService); + Assert.Equal("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", sha256); + }); + + await hostBuilder.StopAsync(); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs new file mode 100644 index 0000000..65eacdb --- /dev/null +++ b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs @@ -0,0 +1,67 @@ +using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; + +using Xunit; + +namespace AdvancedSystems.Security.Tests.Extensions; + +/// +/// Tests the public methods in . +/// +public sealed class CertificateExtensionsTests +{ + #region Tests + + /// + /// Tests that + /// extracts the RDNs from the DN correctly from the string. + /// + [Fact] + public void TestTryParseDistinguishedName() + { + // Arrange + string commonName = "Advanced Systems Root"; + string organizationalUnit = "R&D Department"; + string organization = "Advanced Systems Inc."; + string locality = "Berlin"; + string state = "Berlin"; + string country = "DE"; + string distinguiedName = $"CN={commonName}, OU={organizationalUnit}, O={organization}, L={locality}, S={state}, C={country}"; + + // Act + bool isDn = CertificateExtensions.TryParseDistinguishedName(distinguiedName, out DistinguishedName? dn); + + // Assert + Assert.Multiple(() => + { + Assert.True(isDn); + Assert.NotNull(dn); + Assert.Equal(dn.CommonName, commonName); + Assert.Equal(dn.OrganizationalUnit, organizationalUnit); + Assert.Equal(dn.Organization, organization); + Assert.Equal(dn.Locality, locality); + Assert.Equal(dn.State, state); + Assert.Equal(dn.Country, country); + }); + } + + /// + /// Tests that + /// when the DN is malformed. + /// + [Fact] + public void TestTryParseDistinguishedName_ReturnsNull() + { + // Arrange + string distinguishedName = string.Empty; + + // Act + bool isDn = CertificateExtensions.TryParseDistinguishedName(distinguishedName, out DistinguishedName? dn); + + // Assert + Assert.False(isDn); + Assert.Null(dn); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs index f5fd591..3a4d706 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs @@ -1,25 +1,20 @@ using System.Linq; using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.DependencyInjection; using AdvancedSystems.Security.Options; using AdvancedSystems.Security.Tests.Fixtures; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - using Moq; using Xunit; namespace AdvancedSystems.Security.Tests.Services; -public class CertificateServiceTests : IClassFixture +/// +/// Tests the public methods in . +/// +public sealed class CertificateServiceTests : IClassFixture { private readonly CertificateFixture _sut; @@ -30,6 +25,10 @@ public CertificateServiceTests(CertificateFixture fixture) #region Tests + /// + /// Tests that + /// returns a mocked certificate from the certificate store. + /// [Fact] public void TestGetStoreCertificate() { @@ -43,11 +42,19 @@ public void TestGetStoreCertificate() var certificate = this._sut.CertificateService.GetStoreCertificate(thumbprint, StoreName.My, StoreLocation.CurrentUser); // Assert - Assert.NotNull(certificate); - Assert.Equal(thumbprint, certificate.Thumbprint); + Assert.Multiple(() => + { + Assert.NotNull(certificate); + Assert.Equal(thumbprint, certificate.Thumbprint); + }); + this._sut.Store.Verify(service => service.Open(It.Is(flags => flags == OpenFlags.ReadOnly))); } + /// + /// Tests that + /// returns if a certificate could not be found in the certificate store. + /// [Fact] public void TestGetStoreCertificate_NotFound() { @@ -65,6 +72,10 @@ public void TestGetStoreCertificate_NotFound() Assert.Null(certificate); } + /// + /// Tests that + /// returns a mocked certificate from the certificate store. + /// [Fact] public void GetConfiguredCertificate() { @@ -90,11 +101,19 @@ public void GetConfiguredCertificate() var certificate = this._sut.CertificateService.GetConfiguredCertificate(); // Assert - Assert.NotNull(certificate); - Assert.Equal(certificateOptions.Thumbprint, certificate.Thumbprint); + Assert.Multiple(() => + { + Assert.NotNull(certificate); + Assert.Equal(certificateOptions.Thumbprint, certificate.Thumbprint); + }); + this._sut.Store.Verify(service => service.Open(It.Is(flags => flags == OpenFlags.ReadOnly))); } + /// + /// Tests that + /// returns if a certificate could not be found in the certificate store. + /// [Fact] public void GetConfiguredCertificate_NotFound() { @@ -122,71 +141,5 @@ public void GetConfiguredCertificate_NotFound() Assert.Null(certificate); } - [Fact] - public async Task TestAddCertificateService_FromOptions() - { - // Arrange - using var hostBuilder = await new HostBuilder() - .ConfigureWebHost(builder => builder - .UseTestServer() - .ConfigureServices(services => - { - services.AddCertificateService(options => - { - options.Thumbprint = "123456789"; - options.Store = new CertificateStoreOptions - { - Location = StoreLocation.CurrentUser, - Name = StoreName.My, - }; - }); - }) - .Configure(app => - { - - })) - .StartAsync(); - - // Act - var certificateService = hostBuilder.Services.GetService(); - var certificate = certificateService?.GetConfiguredCertificate(); - - // Assert - Assert.NotNull(certificateService); - Assert.Null(certificate); - await hostBuilder.StopAsync(); - } - - [Fact] - public async Task TestAddCertificateService_FromAppSettings() - { - // Arrange - using var hostBuilder = await new HostBuilder() - .ConfigureWebHost(builder => builder - .UseTestServer() - .ConfigureAppConfiguration(config => - { - config.AddJsonFile("appsettings.json", optional: false); - }) - .ConfigureServices((context, services) => - { - services.AddCertificateService(context.Configuration); - }) - .Configure(app => - { - - })) - .StartAsync(); - - // Act - var certificateService = hostBuilder.Services.GetService(); - var certificate = certificateService?.GetConfiguredCertificate(); - - // Assert - Assert.NotNull(certificateService); - Assert.Null(certificate); - await hostBuilder.StopAsync(); - } - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Services/CertificateStoreTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateStoreTests.cs index 79e4742..e586213 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateStoreTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateStoreTests.cs @@ -1,21 +1,14 @@ -using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; - -using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.DependencyInjection; +using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Tests.Fixtures; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - using Xunit; namespace AdvancedSystems.Security.Tests.Services; -public class CertificateStoreTests : IClassFixture +/// +/// Tests the public methods in . +/// +public sealed class CertificateStoreTests : IClassFixture { private readonly CertificateStoreFixture _sut; @@ -26,64 +19,7 @@ public CertificateStoreTests(CertificateStoreFixture fixture) #region Tests - [Fact] - public async Task TestAddCertificateStore_FromOptions() - { - // Arrange - using var hostBuilder = await new HostBuilder() - .ConfigureWebHost(builder => builder - .UseTestServer() - .ConfigureServices(services => - { - services.AddCertificateStore(options => - { - options.Name = StoreName.My; - options.Location = StoreLocation.CurrentUser; - }); - }) - .Configure(app => - { - - })) - .StartAsync(); - - // Act - var certificateStore = hostBuilder.Services.GetService(); - - // Assert - Assert.NotNull(certificateStore); - await hostBuilder.StopAsync(); - } - - [Fact] - public async Task TestAddCertificateStore_FromAppSettings() - { - // Arrange - using var hostBuilder = await new HostBuilder() - .ConfigureWebHost(builder => builder - .UseTestServer() - .ConfigureAppConfiguration(config => - { - config.AddJsonFile("appsettings.json", optional: false); - }) - .ConfigureServices((context, services) => - { - - services.AddCertificateStore(context.Configuration); - }) - .Configure(app => - { - - })) - .StartAsync(); - - // Act - var certificateStore = hostBuilder.Services.GetService(); - - // Assert - Assert.NotNull(certificateStore); - await hostBuilder.StopAsync(); - } + // TODO #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Services/CryptoRandomServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CryptoRandomServiceTests.cs index 0bb3f2c..df2f796 100644 --- a/AdvancedSystems.Security.Tests/Services/CryptoRandomServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CryptoRandomServiceTests.cs @@ -1,21 +1,20 @@ using System; using System.Linq; -using System.Threading.Tasks; using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.DependencyInjection; using AdvancedSystems.Security.Tests.Fixtures; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - using Xunit; namespace AdvancedSystems.Security.Tests.Services; -public class CryptoRandomServiceTests : IClassFixture +/// +/// Tests the public methods in . +/// +/// +/// These methods are more exhaustively tested by the underlying provider class. +/// +public sealed class CryptoRandomServiceTests : IClassFixture { private readonly CryptoRandomFixture _sut; @@ -26,6 +25,10 @@ public CryptoRandomServiceTests(CryptoRandomFixture fixture) #region Tests + /// + /// Tests that returns + /// an array of elements of the correct size. + /// [Fact] public void TestGetBytes() { @@ -91,35 +94,11 @@ public void TestChoice() int randomNumber = this._sut.CryptoRandomService.Choice(array); // Assert - Assert.Contains(randomNumber, array); - Assert.InRange(randomNumber, min, max - 1); - } - - [Fact] - public async Task TestAddCryptoRandomService() - { - // Arrange - using var hostBuilder = await new HostBuilder() - .ConfigureWebHost(builder => - { - builder.UseTestServer(); - builder.ConfigureServices(services => - { - services.AddCryptoRandomService(); - }); - builder.Configure(app => - { - - }); - }) - .StartAsync(); - - // Act - var cryptoRandomService = hostBuilder.Services.GetService(); - - // Assert - Assert.NotNull(cryptoRandomService); - await hostBuilder.StopAsync(); + Assert.Multiple(() => + { + Assert.Contains(randomNumber, array); + Assert.InRange(randomNumber, min, max - 1); + }); } #endregion diff --git a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs index 3bea864..243a4c5 100644 --- a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs @@ -1,15 +1,9 @@ using System; using System.Text; -using System.Threading.Tasks; using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.DependencyInjection; using AdvancedSystems.Security.Tests.Fixtures; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Moq; @@ -18,7 +12,13 @@ namespace AdvancedSystems.Security.Tests.Services; -public class HashServiceTests : IClassFixture +/// +/// Tests the public methods in . +/// +/// +/// These methods are more exhaustively tested by the underlying provider class. +/// +public sealed class HashServiceTests : IClassFixture { private readonly HashServiceFixture _sut; @@ -29,6 +29,10 @@ public HashServiceTests(HashServiceFixture fixture) #region Tests + /// + /// Tests that returns the expected hash, + /// and that the log warning message is called. + /// [Fact] public void TestMD5Hash() { @@ -52,6 +56,10 @@ public void TestMD5Hash() ); } + /// + /// Tests that returns the expected hash, + /// and that the log warning message is called. + /// [Fact] public void TestSHA1Hash() { @@ -75,6 +83,9 @@ public void TestSHA1Hash() ); } + /// + /// Tests that returns the expected hash. + /// [Fact] public void TestSHA256Hash() { @@ -89,6 +100,9 @@ public void TestSHA256Hash() Assert.Equal("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", sha256); } + /// + /// Tests that returns the expected hash. + /// [Fact] public void TestSHA384Hash() { @@ -103,6 +117,9 @@ public void TestSHA384Hash() Assert.Equal("ca737f1014a48f4c0b6dd43cb177b0afd9e5169367544c494011e3317dbf9a509cb1e5dc1e85a941bbee3d7f2afbc9b1", sha384); } + /// + /// Tests that returns the expected hash. + /// [Fact] public void TestSHA512Hash() { @@ -117,35 +134,5 @@ public void TestSHA512Hash() Assert.Equal("07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6", sha512); } - [Fact] - public async Task TestAddHashService() - { - // Arrange - using var hostBuilder = await new HostBuilder() - .ConfigureWebHost(builder => builder - .UseTestServer() - .ConfigureServices(services => - { - services.AddHashService(); - }) - .Configure(app => - { - - })) - .StartAsync(); - - string input = "The quick brown fox jumps over the lazy dog"; - byte[] buffer = Encoding.UTF8.GetBytes(input); - - // Act - var hashService = hostBuilder.Services.GetService(); - string? sha256 = hashService?.GetSHA256Hash(buffer); - - // Assert - Assert.NotNull(hashService); - Assert.Equal("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", sha256); - await hostBuilder.StopAsync(); - } - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security/AdvancedSystems.Security.csproj b/AdvancedSystems.Security/AdvancedSystems.Security.csproj index 2326e78..11b168d 100644 --- a/AdvancedSystems.Security/AdvancedSystems.Security.csproj +++ b/AdvancedSystems.Security/AdvancedSystems.Security.csproj @@ -10,7 +10,7 @@ - + diff --git a/AdvancedSystems.Security/Cryptography/DistinguishedName.cs b/AdvancedSystems.Security/Cryptography/DistinguishedName.cs new file mode 100644 index 0000000..5d07ce1 --- /dev/null +++ b/AdvancedSystems.Security/Cryptography/DistinguishedName.cs @@ -0,0 +1,61 @@ +using System.Security.Cryptography.X509Certificates; + +namespace AdvancedSystems.Security.Cryptography; + +/// +/// Defines the Distinguished Name (DN) of an entity that owns or issued the certificate. +/// This entity is typically a Certificate Authority (CA) or an intermediate CA in a chain of trust. +/// +/// +/// DN is a term that describes the identifying information in a certificate and is +/// part of the itself. A certificate contains DN +/// information for both the owner or requestor of the certificate (called the Subject DN) +/// and the CA that issues the certificate (called the Issuer DN). Depending on the +/// identification policy of the CA that issues a certificate, the DN can include a variety +/// of information. +/// +/// +public sealed record DistinguishedName +{ + /// + /// Gets or sets the . + /// + /// + /// + /// + public string? Country { get; init; } + + /// + /// Gets or sets the . + /// + /// + /// + /// + public string? CommonName { get; init; } + + /// + /// Gets or sets the . + /// + /// + /// + /// + public string? Locality { get; init; } + + /// + /// Gets or sets the . + /// + public string? Organization { get; init; } + + /// + /// Gets or sets the . + /// + public string? OrganizationalUnit { get; init; } + + /// + /// Gets or sets the .. + /// + /// + /// + /// + public string? State { get; init; } +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Cryptography/RDN.cs b/AdvancedSystems.Security/Cryptography/RDN.cs new file mode 100644 index 0000000..4dc8de1 --- /dev/null +++ b/AdvancedSystems.Security/Cryptography/RDN.cs @@ -0,0 +1,55 @@ +namespace AdvancedSystems.Security.Cryptography; + +/// +/// The Relative Distinguished Names (RDNs) are attribute-value pairs that +/// uniquely identify an entity. +/// +/// +/// This class defines the attributes that annotate one or more values. +/// +/// +/// +public static class RDN +{ + /// + /// Country (C) + /// + /// + /// This field could also be used to store the region. + /// + public const string C = "C"; + + /// + /// Common Name (CN) + /// + /// + /// This field denotes the certificate owner's common name. + /// + public const string CN = "CN"; + + /// + /// Locality (L) + /// + /// + /// This field could also be used to store the city. + /// + public const string L = "L"; + + /// + /// Organization (O) + /// + public const string O = "O"; + + /// + /// Organizational Unit (OU) + /// + public const string OU = "OU"; + + /// + /// State (S) + /// + /// + /// This field could also be used to store the province. + /// + public const string S = "S"; +} \ No newline at end of file diff --git a/AdvancedSystems.Security/DependencyInjection/Sections.cs b/AdvancedSystems.Security/DependencyInjection/Sections.cs new file mode 100644 index 0000000..21a9db9 --- /dev/null +++ b/AdvancedSystems.Security/DependencyInjection/Sections.cs @@ -0,0 +1,24 @@ +using AdvancedSystems.Security.Options; + +namespace AdvancedSystems.Security.DependencyInjection; + +/// +/// Defines section keys for appsettings.json. +/// +public static partial class Sections +{ + /// + /// Key used to bind the configuration section. + /// + public const string CERTIFICATE = "Certificate"; + + /// + /// Key used to bind the configuration section. + /// + public const string RSA = "RSA"; + + /// + /// Key used to bind the configuration section. + /// + public const string STORE = "Store"; +} \ No newline at end of file diff --git a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs index 51165e7..5dac396 100644 --- a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using System; +using AdvancedSystems.Core.DependencyInjection; using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Options; using AdvancedSystems.Security.Services; @@ -11,10 +12,19 @@ namespace AdvancedSystems.Security.DependencyInjection; -public static class ServiceCollectionExtensions +public static partial class ServiceCollectionExtensions { #region CryptoRandom + /// + /// Adds the default implementation of to . + /// + /// + /// The service collection containing the service. + /// + /// + /// The value of . + /// public static IServiceCollection AddCryptoRandomService(this IServiceCollection services) { services.TryAdd(ServiceDescriptor.Scoped()); @@ -25,6 +35,15 @@ public static IServiceCollection AddCryptoRandomService(this IServiceCollection #region HashService + /// + /// Adds the default implementation of to . + /// + /// + /// The service collection containing the service. + /// + /// + /// The value of . + /// public static IServiceCollection AddHashService(this IServiceCollection services) { services.TryAdd(ServiceDescriptor.Scoped()); @@ -35,7 +54,7 @@ public static IServiceCollection AddHashService(this IServiceCollection services #region CertificateStore - internal static void AddCertificateStore(this IServiceCollection services) where TOptions : class + private static void AddCertificateStore(this IServiceCollection services) where TOptions : class { services.TryAdd(ServiceDescriptor.Singleton(serviceProvider => { @@ -50,6 +69,18 @@ internal static void AddCertificateStore(this IServiceCollection servi })); } + /// + /// Adds the default implementation of to . + /// + /// + /// The service collection containing the service. + /// + /// + /// An action used to configure . + /// + /// + /// The value of . + /// public static IServiceCollection AddCertificateStore(this IServiceCollection services, Action setupAction) { services.AddOptions() @@ -59,13 +90,21 @@ public static IServiceCollection AddCertificateStore(this IServiceCollection ser return services; } - public static IServiceCollection AddCertificateStore(this IServiceCollection services, IConfiguration configuration) + /// + /// Adds the default implementation of to . + /// + /// + /// The service collection containing the service. + /// + /// + /// A configuration section targeting . + /// + /// + /// The value of . + /// + public static IServiceCollection AddCertificateStore(this IServiceCollection services, IConfigurationSection configurationSection) { - services.AddOptions() - .Bind(configuration.GetRequiredSection(Sections.CERTIFICATE_STORE)) - .ValidateDataAnnotations() - .ValidateOnStart(); - + services.TryAddOptions(configurationSection); services.AddCertificateStore(); return services; } @@ -74,11 +113,23 @@ public static IServiceCollection AddCertificateStore(this IServiceCollection ser #region CertificateService - internal static void AddCertificateService(this IServiceCollection services) + private static void AddCertificateService(this IServiceCollection services) { services.TryAdd(ServiceDescriptor.Scoped()); } + /// + /// Adds the default implementation of to . + /// + /// + /// The service collection containing the service. + /// + /// + /// An action used to configure . + /// + /// + /// The value of . + /// public static IServiceCollection AddCertificateService(this IServiceCollection services, Action setupAction) { services.AddOptions() @@ -90,14 +141,23 @@ public static IServiceCollection AddCertificateService(this IServiceCollection s return services; } - public static IServiceCollection AddCertificateService(this IServiceCollection services, IConfiguration configuration) + /// + /// Adds the default implementation of to . + /// + /// + /// The service collection containing the service. + /// + /// + /// A configuration section targeting . NOTE: This configuration requires a nested + /// section within the previous section. + /// + /// + /// The value of . + /// + public static IServiceCollection AddCertificateService(this IServiceCollection services, IConfigurationSection configurationSection) { - services.AddOptions() - .Bind(configuration.GetRequiredSection(Sections.CERTIFICATE)) - .ValidateDataAnnotations() - .ValidateOnStart(); - - services.AddCertificateStore(configuration); + services.TryAddOptions(configurationSection); + services.AddCertificateStore(configurationSection.GetRequiredSection(Sections.STORE)); services.AddCertificateService(); return services; @@ -107,16 +167,41 @@ public static IServiceCollection AddCertificateService(this IServiceCollection s #region RSACryptoService - internal static IServiceCollection AddRSACryptoService(this IServiceCollection services) + private static IServiceCollection AddRSACryptoService(this IServiceCollection services) { throw new NotImplementedException(); } + + /// + /// Adds the default implementation of to . + /// + /// + /// The service collection containing the service. + /// + /// + /// An action used to configure . + /// + /// + /// The value of . + /// public static IServiceCollection AddRSACryptoService(this IServiceCollection services, Action setupAction) { throw new NotImplementedException(); } - public static IServiceCollection AddRSACryptoService(this IServiceCollection services, IConfiguration configuration) + /// + /// Adds the default implementation of to . + /// + /// + /// The service collection containing the service. + /// + /// + /// A configuration section targeting . + /// + /// + /// The value of . + /// + public static IServiceCollection AddRSACryptoService(this IServiceCollection services, IConfigurationSection configurationSection) { throw new NotImplementedException(); } diff --git a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs new file mode 100644 index 0000000..27086b2 --- /dev/null +++ b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Abstractions.Exceptions; +using AdvancedSystems.Security.Cryptography; + +using static System.Net.WebRequestMethods; + +namespace AdvancedSystems.Security.Extensions; + +/// +/// Defines functions for interacting with X.509 certificates. +/// +/// +public static partial class CertificateExtensions +{ + /// + /// Retrieves an X.509 certificate from the specified store using the provided thumbprint. + /// + /// + /// The type of the certificate store, which must implement the interface. + /// + /// + /// The certificate store from which to retrieve the certificate. + /// + /// + /// The thumbprint of the certificate to locate. + /// + /// + /// The object if the certificate is found. + /// + /// + /// Thrown when no certificate with the specified thumbprint is found in the store. + /// + public static X509Certificate2 GetCertificate(this T store, string thumbprint) where T : ICertificateStore + { + store.Open(OpenFlags.ReadOnly); + + var certificate = store.Certificates + .Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) + .OfType() + .FirstOrDefault(); + + return certificate + ?? throw new CertificateNotFoundException("No valid certificate matching the search criteria could be found in the store."); + } + + /// + /// Attempts to parse the specified distinguished name (DN) string into a object. + /// + /// + /// The DN string to parse, typically in the X.500 or LDAP DN format. + /// + /// + /// When this method returns, contains the parsed object if the parsing was successful; + /// otherwise, . + /// + /// + /// if the parsing was successful; otherwise, . + /// + /// + /// The X.500 Distinguished Name (DN) and the LDAP Distinguished Name (DN) differ in syntax and conventions. + /// + /// + /// + /// X.500 Format + /// + /// + /// Comes from the X.500 standard for directory services + /// + /// + /// + /// + /// Separator + /// + /// + /// Components are separated by commas (,). + /// + /// + /// + /// + /// Order + /// + /// + /// Attributes are typically listed in the most significant to least significant order + /// (root to leaf). + /// + /// + /// + /// + /// Attributes + /// + /// + /// Attributes are case-insensitive but are conventionally written in uppercase. Note that + /// attribute names are more verbose in some X.500 implementations. + /// + /// + /// + /// + /// + /// + /// LDAP + /// + /// + /// A streamlined version of the X.500 DN, tailored for LDAP (Lightweight Directory Access Protocol). + /// + /// + /// + /// + /// Separator + /// + /// + /// Components are separated by commas (,), like X.500, but semicolons (;) are also sometimes + /// allowed (albeit less common). + /// + /// + /// + /// + /// Order + /// + /// + /// Attributes are typically listed in the least significant to most significant order (leaf to root), + /// though many implementations allow flexibility. + /// + /// + /// + /// + /// Attributes + /// + /// + /// LDAP uses abbreviated attribute names (e.g., CN for common name, O for organization, OU for organizational unit). + /// It is also more permissive with regards to case sensitivity and attribute formatting. + /// + /// + /// + /// + /// + public static bool TryParseDistinguishedName(string distinguishedName, out DistinguishedName? result) + { + var rdns = new Dictionary(StringComparer.OrdinalIgnoreCase); + var dn = new X500DistinguishedName(distinguishedName); + + foreach (var rdn in dn.EnumerateRelativeDistinguishedNames()) + { + string? attribute = rdn.GetSingleElementType().FriendlyName; + string value = rdn.GetSingleElementValue() ?? string.Empty; + + if (string.IsNullOrEmpty(attribute)) continue; + + rdns.Add(attribute, value); + } + + bool hasCountry = rdns.TryGetValue(RDN.C, out string? country); + bool hasCommonName = rdns.TryGetValue(RDN.CN, out string? commonName); + bool hasLocality = rdns.TryGetValue(RDN.L, out string? locality); + bool hasOrganization = rdns.TryGetValue(RDN.O, out string? organization); + bool hasOrganizationalUnit = rdns.TryGetValue(RDN.OU, out string? organizationalUnit); + bool hasState = rdns.TryGetValue(RDN.S, out string? state); + + bool hasRelativeDistinguishedNames = hasCountry + || hasCommonName + || hasLocality + || hasOrganization + || hasOrganizationalUnit + || hasState; + + result = hasRelativeDistinguishedNames ? new DistinguishedName + { + CommonName = commonName, + OrganizationalUnit = organizationalUnit, + Organization = organization, + Locality = locality, + State = state, + Country = country, + } : null; + + return hasRelativeDistinguishedNames; + } +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Extensions/CertificateStore.cs b/AdvancedSystems.Security/Extensions/CertificateStore.cs deleted file mode 100644 index 2e4afd4..0000000 --- a/AdvancedSystems.Security/Extensions/CertificateStore.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Linq; -using System.Security.Cryptography.X509Certificates; - -using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.Abstractions.Exceptions; - -namespace AdvancedSystems.Security.Extensions; - -/// -/// Defines functions for interacting with X.509 certificates. -/// -/// -public static class CertificateStore -{ - /// - /// Retrieves an X.509 certificate from the specified store using the provided thumbprint. - /// - /// - /// The type of the certificate store, which must implement the interface. - /// - /// - /// The certificate store from which to retrieve the certificate. - /// - /// - /// The thumbprint of the certificate to locate. - /// - /// - /// The object if the certificate is found. - /// - /// - /// Thrown when no certificate with the specified thumbprint is found in the store. - /// - public static X509Certificate2 GetCertificate(this T store, string thumbprint) where T : ICertificateStore - { - store.Open(OpenFlags.ReadOnly); - - var certificate = store.Certificates - .Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) - .OfType() - .FirstOrDefault(); - - return certificate - ?? throw new CertificateNotFoundException("No valid certificate matching the search criteria could be found in the store."); - } -} \ No newline at end of file diff --git a/AdvancedSystems.Security/Options/CertificateOptions.cs b/AdvancedSystems.Security/Options/CertificateOptions.cs index 5296e4e..505c02b 100644 --- a/AdvancedSystems.Security/Options/CertificateOptions.cs +++ b/AdvancedSystems.Security/Options/CertificateOptions.cs @@ -2,7 +2,7 @@ namespace AdvancedSystems.Security.Options; -public sealed class CertificateOptions +public sealed record CertificateOptions { [Key] [Required(AllowEmptyStrings = false)] diff --git a/AdvancedSystems.Security/Options/CertificateStoreOptions.cs b/AdvancedSystems.Security/Options/CertificateStoreOptions.cs index 8e163ca..dde2e45 100644 --- a/AdvancedSystems.Security/Options/CertificateStoreOptions.cs +++ b/AdvancedSystems.Security/Options/CertificateStoreOptions.cs @@ -3,7 +3,7 @@ namespace AdvancedSystems.Security.Options; -public sealed class CertificateStoreOptions +public sealed record CertificateStoreOptions { [Required] [EnumDataType(typeof(StoreName))] diff --git a/AdvancedSystems.Security/Options/RSACryptoOptions.cs b/AdvancedSystems.Security/Options/RSACryptoOptions.cs index 308c0c7..3e24f72 100644 --- a/AdvancedSystems.Security/Options/RSACryptoOptions.cs +++ b/AdvancedSystems.Security/Options/RSACryptoOptions.cs @@ -3,7 +3,7 @@ namespace AdvancedSystems.Security.Options; -public sealed class RSACryptoOptions +public sealed record RSACryptoOptions { public required HashAlgorithmName HashAlgorithmName { get; set; } diff --git a/AdvancedSystems.Security/Options/Sections.cs b/AdvancedSystems.Security/Options/Sections.cs deleted file mode 100644 index 940d22a..0000000 --- a/AdvancedSystems.Security/Options/Sections.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace AdvancedSystems.Security.Options; - -public readonly record struct Sections -{ - public const string CERTIFICATE_STORE = "Certificate:Store"; - - public const string CERTIFICATE = "Certificate"; - - public const string RSA = "RSA"; -} \ No newline at end of file