From a811d7b7bbec93e4a88aa06fc2daf55a8060f747 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 22 Nov 2024 22:20:53 +0100 Subject: [PATCH 01/41] Make CertificateOptions more permissive: allow empty configurations --- .../ServiceCollectionExtensions.cs | 11 +++++++---- .../Options/CertificateOptions.cs | 7 ++----- AdvancedSystems.Security/Options/RSACryptoOptions.cs | 7 ++++++- .../Services/CertificateService.cs | 6 ++++++ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs index 5dac396..f2204d1 100644 --- a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Core.DependencyInjection; using AdvancedSystems.Security.Abstractions; @@ -54,18 +55,20 @@ public static IServiceCollection AddHashService(this IServiceCollection services #region CertificateStore + private record CertificateOptionsCarrier(StoreName StoreName, StoreLocation StoreLocation); + private static void AddCertificateStore(this IServiceCollection services) where TOptions : class { services.TryAdd(ServiceDescriptor.Singleton(serviceProvider => { - var options = serviceProvider.GetRequiredService>().Value switch + CertificateOptionsCarrier options = serviceProvider.GetRequiredService>().Value switch { - CertificateOptions certificateOptions => new { certificateOptions.Store.Name, certificateOptions.Store.Location }, - CertificateStoreOptions storeOptions => new { storeOptions.Name, storeOptions.Location }, + CertificateOptions certificateOptions => new(certificateOptions.Store?.Name ?? throw new ArgumentNullException(nameof(CertificateOptionsCarrier.StoreName)), certificateOptions.Store?.Location ?? throw new ArgumentNullException(nameof(CertificateOptionsCarrier.StoreLocation))), + CertificateStoreOptions storeOptions => new(storeOptions.Name, storeOptions.Location), _ => throw new NotImplementedException() }; - return new CertificateStore(options.Name, options.Location); + return new CertificateStore(options.StoreName, options.StoreLocation); })); } diff --git a/AdvancedSystems.Security/Options/CertificateOptions.cs b/AdvancedSystems.Security/Options/CertificateOptions.cs index 505c02b..088462e 100644 --- a/AdvancedSystems.Security/Options/CertificateOptions.cs +++ b/AdvancedSystems.Security/Options/CertificateOptions.cs @@ -4,10 +4,7 @@ namespace AdvancedSystems.Security.Options; public sealed record CertificateOptions { - [Key] - [Required(AllowEmptyStrings = false)] - public required string Thumbprint { get; set; } + public string? Thumbprint { get; set; } - [Required] - public required CertificateStoreOptions Store { get; set; } + public CertificateStoreOptions? Store { get; set; } } \ No newline at end of file diff --git a/AdvancedSystems.Security/Options/RSACryptoOptions.cs b/AdvancedSystems.Security/Options/RSACryptoOptions.cs index 3e24f72..77b3975 100644 --- a/AdvancedSystems.Security/Options/RSACryptoOptions.cs +++ b/AdvancedSystems.Security/Options/RSACryptoOptions.cs @@ -1,15 +1,20 @@ -using System.Security.Cryptography; +using System.ComponentModel.DataAnnotations; +using System.Security.Cryptography; using System.Text; namespace AdvancedSystems.Security.Options; public sealed record RSACryptoOptions { + [Required] public required HashAlgorithmName HashAlgorithmName { get; set; } + [Required] public required RSAEncryptionPadding EncryptionPadding { get; set; } + [Required] public required RSASignaturePadding SignaturePadding { get; set; } + [Required] public required Encoding Encoding { get; set; } } \ No newline at end of file diff --git a/AdvancedSystems.Security/Services/CertificateService.cs b/AdvancedSystems.Security/Services/CertificateService.cs index 9cf09bb..d6adcf8 100644 --- a/AdvancedSystems.Security/Services/CertificateService.cs +++ b/AdvancedSystems.Security/Services/CertificateService.cs @@ -46,6 +46,12 @@ public CertificateService(ILogger logger, IOptions Date: Wed, 8 Jan 2025 21:18:48 +0100 Subject: [PATCH 02/41] Implement CertificateOptionsValidator and refactor service registration --- .../ServiceCollectionExtensionsTests.cs | 6 +-- .../appsettings.json | 2 +- .../ServiceCollectionExtensions.cs | 39 +++++++++----- .../Extensions/CertificateExtensions.cs | 4 +- .../Services/RSACryptoService.cs | 7 ++- .../Validators/CertificateOptionsValidator.cs | 50 ++++++++++++++++++ test.cer | Bin 0 -> 786 bytes 7 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs create mode 100644 test.cer diff --git a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index f20fcfa..b1e65c5 100644 --- a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -37,7 +37,7 @@ public async Task TestAddCertificateService_FromOptions() { services.AddCertificateService(options => { - options.Thumbprint = "123456789"; + options.Thumbprint = "A24421E3B4149A12B219AA67CD263D419829BD53"; options.Store = new CertificateStoreOptions { Location = StoreLocation.CurrentUser, @@ -59,7 +59,7 @@ public async Task TestAddCertificateService_FromOptions() Assert.Multiple(() => { Assert.NotNull(certificateService); - Assert.Null(certificate); + Assert.NotNull(certificate); }); await hostBuilder.StopAsync(); @@ -97,7 +97,7 @@ public async Task TestAddCertificateService_FromAppSettings() Assert.Multiple(() => { Assert.NotNull(certificateService); - Assert.Null(certificate); + Assert.NotNull(certificate); }); await hostBuilder.StopAsync(); diff --git a/AdvancedSystems.Security.Tests/appsettings.json b/AdvancedSystems.Security.Tests/appsettings.json index bd97412..844840d 100644 --- a/AdvancedSystems.Security.Tests/appsettings.json +++ b/AdvancedSystems.Security.Tests/appsettings.json @@ -1,6 +1,6 @@ { "Certificate": { - "Thumbprint": "123456789", + "Thumbprint": "A24421E3B4149A12B219AA67CD263D419829BD53", "Store": { "Location": "CurrentUser", "Name": "My" diff --git a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs index f2204d1..7443ff7 100644 --- a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs @@ -5,11 +5,13 @@ using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Options; using AdvancedSystems.Security.Services; +using AdvancedSystems.Security.Validators; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using Microsoft.VisualBasic.FileIO; namespace AdvancedSystems.Security.DependencyInjection; @@ -57,19 +59,15 @@ public static IServiceCollection AddHashService(this IServiceCollection services private record CertificateOptionsCarrier(StoreName StoreName, StoreLocation StoreLocation); - private static void AddCertificateStore(this IServiceCollection services) where TOptions : class + private static IServiceCollection AddCertificateStore(this IServiceCollection services) { services.TryAdd(ServiceDescriptor.Singleton(serviceProvider => { - CertificateOptionsCarrier options = serviceProvider.GetRequiredService>().Value switch - { - CertificateOptions certificateOptions => new(certificateOptions.Store?.Name ?? throw new ArgumentNullException(nameof(CertificateOptionsCarrier.StoreName)), certificateOptions.Store?.Location ?? throw new ArgumentNullException(nameof(CertificateOptionsCarrier.StoreLocation))), - CertificateStoreOptions storeOptions => new(storeOptions.Name, storeOptions.Location), - _ => throw new NotImplementedException() - }; - - return new CertificateStore(options.StoreName, options.StoreLocation); + var options = serviceProvider.GetRequiredService>().Value; + return new CertificateStore(options.Name, options.Location); })); + + return services; } /// @@ -89,7 +87,7 @@ public static IServiceCollection AddCertificateStore(this IServiceCollection ser services.AddOptions() .Configure(setupAction); - services.AddCertificateStore(); + services.AddCertificateStore(); return services; } @@ -108,7 +106,7 @@ public static IServiceCollection AddCertificateStore(this IServiceCollection ser public static IServiceCollection AddCertificateStore(this IServiceCollection services, IConfigurationSection configurationSection) { services.TryAddOptions(configurationSection); - services.AddCertificateStore(); + services.AddCertificateStore(); return services; } @@ -116,9 +114,12 @@ public static IServiceCollection AddCertificateStore(this IServiceCollection ser #region CertificateService - private static void AddCertificateService(this IServiceCollection services) + private static IServiceCollection AddCertificateService(this IServiceCollection services) { services.TryAdd(ServiceDescriptor.Scoped()); + services.TryAdd(ServiceDescriptor.Singleton, CertificateOptionsValidator>()); + + return services; } /// @@ -138,7 +139,19 @@ public static IServiceCollection AddCertificateService(this IServiceCollection s services.AddOptions() .Configure(setupAction); - services.AddCertificateStore(); + services.Configure(options => + { + var certificateOptions = new CertificateOptions(); + setupAction.Invoke(certificateOptions); + + var store = certificateOptions.Store + ?? throw new ArgumentNullException(nameof(setupAction), $"{nameof(CertificateStoreOptions)} settings are undefined."); + + options.Name = store.Name; + options.Location = store.Location; + }); + + services.AddCertificateStore(); services.AddCertificateService(); return services; diff --git a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs index 27086b2..e7e2576 100644 --- a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs +++ b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs @@ -7,8 +7,6 @@ using AdvancedSystems.Security.Abstractions.Exceptions; using AdvancedSystems.Security.Cryptography; -using static System.Net.WebRequestMethods; - namespace AdvancedSystems.Security.Extensions; /// @@ -45,7 +43,7 @@ public static X509Certificate2 GetCertificate(this T store, string thumbprint .FirstOrDefault(); return certificate - ?? throw new CertificateNotFoundException("No valid certificate matching the search criteria could be found in the store."); + ?? throw new CertificateNotFoundException($"""No valid certificate with thumbprint "{thumbprint}" could be found in the store."""); } /// diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs index b9aa3cd..2b1fad3 100644 --- a/AdvancedSystems.Security/Services/RSACryptoService.cs +++ b/AdvancedSystems.Security/Services/RSACryptoService.cs @@ -29,13 +29,14 @@ public RSACryptoService(ILogger logger, ICertificateService ce this._certificateService = certificateService; this._options = options; - this._certificate = this._certificateService.GetConfiguredCertificate() - ?? throw new ArgumentNullException(nameof(this._certificate)); + this._certificate = this._certificateService.GetConfiguredCertificate()!; var config = this._options.Value; this._provider = new RSACryptoProvider(this._certificate, config.HashAlgorithmName, config.EncryptionPadding, config.SignaturePadding, config.Encoding); } + #region Implementation + #region Properties /// @@ -135,4 +136,6 @@ public bool VerifyData(string data, string signature, Encoding? encoding = null) } #endregion + + #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs b/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs new file mode 100644 index 0000000..658fe44 --- /dev/null +++ b/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs @@ -0,0 +1,50 @@ +using System.Security.Cryptography.X509Certificates; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Abstractions.Exceptions; +using AdvancedSystems.Security.Extensions; +using AdvancedSystems.Security.Options; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AdvancedSystems.Security.Validators; + +public sealed class CertificateOptionsValidator : IValidateOptions +{ + private readonly ILogger _logger; + private readonly ICertificateStore _certificateStore; + + public CertificateOptionsValidator(ILogger logger, ICertificateStore certificateStore) + { + this._logger = logger; + this._certificateStore = certificateStore; + } + + #region Implementation + + public ValidateOptionsResult Validate(string? name, CertificateOptions options) + { + this._logger.LogDebug("Started validation of {Options}", nameof(CertificateOptions)); + + if (string.IsNullOrEmpty(options.Thumbprint)) + { + return ValidateOptionsResult.Fail("Thumbprint is null or empty."); + } + + try + { + X509Certificate2 certificate = this._certificateStore.GetCertificate(options.Thumbprint); + } + catch (CertificateNotFoundException exception) + { + return ValidateOptionsResult.Fail(exception.Message); + } + + this._logger.LogDebug("Completed validation of {Options}", nameof(CertificateOptions)); + + return ValidateOptionsResult.Success; + } + + #endregion +} \ No newline at end of file diff --git a/test.cer b/test.cer new file mode 100644 index 0000000000000000000000000000000000000000..0394a93ae93c6338656bc4f0a491d96a2584905c GIT binary patch literal 786 zcmXqLV&*ewV*Iv%nTe5!Ng#5KLQwM5OIw`%x2_4}UpLQymyJ`a&7h&j5587gG}>Bg5gYz#a}J`AoZqD-NXI=MXsLH#fiS=h|b_&Yux= z<2$9H7Ss^V=M16?|W#XVz6h~?=u2VT=#SD zHdy31C9!6C$@3|Th0ojlv9Z*cV4Or{@N-%b%k2l z7lpa|LX3VdZ#-rox#F9ngw4%adzwNoU2pMLk?H%(#LURRxH!PT&wvjY`m+3tjQ?3! zn3-4?7|4S7sw`p#B5WMmY>cd|?95DX79&KOk420{#Po`hyk5rQ#3G~kdFC_jv{=TQ zdLxH9FnWPu&d9JX|74_8Qo=`}?V=pLY;q?Ir(Tbr=e{nf+gB*~L#pF9YlhsJI)`=N zY>NJJe{uJ{-t@O+A3t5WdF;)Ojg8X&^)nxy?aR6$Sf9^SlJHR>z2eI<{_T6enJ`#+=LrrfS?9*bd2F+v0R?%Qeeir%#k6Xl`0~t!t-X ZjqA~dectO_CZ}BUHu{?V>p;u2MgYq^IR*d# literal 0 HcmV?d00001 From 364a576cbe315289499442b62e3a0a48df78cce9 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 8 Jan 2025 21:34:15 +0100 Subject: [PATCH 03/41] Import test certificate in GitHub Action runners --- .github/workflows/dotnet-tests.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index 1160b99..bc119a1 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -31,5 +31,21 @@ jobs: - name: Build run: dotnet build --no-restore + - name: Import Test Certificate (Windows) + if: runner.os == 'Windows' + run: | + certutil -addstore -f "ROOT" test.cer + + - name: Import Test Certificate (Ubuntu) + if: runner.os == 'Linux' + run: | + sudo cp test.cer /usr/local/share/ca-certificates + sudo update-ca-certificates + + - name: Import Test Certificate (MacOS) + if: runner.os == 'macOS' + run: | + sudo security add-trusted-cert -d -r trustAsRoot -k /Library/Keychains/System.keychain test.cer + - name: Test run: dotnet test --no-build --verbosity normal From e00b956061fb3547cd5427ec1535570541d2e7df Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 8 Jan 2025 21:51:22 +0100 Subject: [PATCH 04/41] Add validOnly (default = true) search parameter to ICertificateService interface --- .../ICertificateService.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/AdvancedSystems.Security.Abstractions/ICertificateService.cs b/AdvancedSystems.Security.Abstractions/ICertificateService.cs index b578fa9..06dc013 100644 --- a/AdvancedSystems.Security.Abstractions/ICertificateService.cs +++ b/AdvancedSystems.Security.Abstractions/ICertificateService.cs @@ -25,21 +25,27 @@ public interface ICertificateService /// The location of the certificate store, such as /// or . /// + /// + /// to allow only valid certificates to be returned from the search; otherwise, . + /// /// /// The object if the certificate is found, else null. /// /// /// Thrown when no certificate with the specified thumbprint is found in the store. /// - X509Certificate2? GetStoreCertificate(string thumbprint, StoreName storeName, StoreLocation storeLocation); + X509Certificate2? GetStoreCertificate(string thumbprint, StoreName storeName, StoreLocation storeLocation, bool validOnly = true); /// /// Retrieves an application-configured X.509 certificate. /// + /// + /// to allow only valid certificates to be returned from the search; otherwise, . + /// /// /// The object if the certificate is found, else null. /// - X509Certificate2? GetConfiguredCertificate(); + X509Certificate2? GetConfiguredCertificate(bool validOnly = true); #endregion } \ No newline at end of file From 1ae444a78c0f9a41646b0d1109e851db8bb110ef Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 8 Jan 2025 22:19:22 +0100 Subject: [PATCH 05/41] Implement validOnly flag (and turn it off for testing purposes only) --- .../ServiceCollectionExtensionsTests.cs | 4 ++-- .../Services/CertificateServiceTests.cs | 16 ++++++++-------- .../AdvancedSystems.Security.csproj | 2 +- .../Extensions/CertificateExtensions.cs | 9 ++++++--- .../Services/CertificateService.cs | 8 ++++---- .../Validators/CertificateOptionsValidator.cs | 2 +- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index b1e65c5..6c238f0 100644 --- a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -53,7 +53,7 @@ public async Task TestAddCertificateService_FromOptions() // Act var certificateService = hostBuilder.Services.GetService(); - var certificate = certificateService?.GetConfiguredCertificate(); + var certificate = certificateService?.GetConfiguredCertificate(validOnly: false); // Assert Assert.Multiple(() => @@ -91,7 +91,7 @@ public async Task TestAddCertificateService_FromAppSettings() // Act var certificateService = hostBuilder.Services.GetService(); - var certificate = certificateService?.GetConfiguredCertificate(); + var certificate = certificateService?.GetConfiguredCertificate(validOnly: false); // Assert Assert.Multiple(() => diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs index 3a4d706..ea3a407 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs @@ -26,7 +26,7 @@ public CertificateServiceTests(CertificateFixture fixture) #region Tests /// - /// Tests that + /// Tests that /// returns a mocked certificate from the certificate store. /// [Fact] @@ -39,7 +39,7 @@ public void TestGetStoreCertificate() .Returns(certificates); // Act - var certificate = this._sut.CertificateService.GetStoreCertificate(thumbprint, StoreName.My, StoreLocation.CurrentUser); + var certificate = this._sut.CertificateService.GetStoreCertificate(thumbprint, StoreName.My, StoreLocation.CurrentUser, validOnly: false); // Assert Assert.Multiple(() => @@ -52,7 +52,7 @@ public void TestGetStoreCertificate() } /// - /// Tests that + /// Tests that /// returns if a certificate could not be found in the certificate store. /// [Fact] @@ -66,14 +66,14 @@ public void TestGetStoreCertificate_NotFound() .Returns(new X509Certificate2Collection()); // Act - var certificate = this._sut.CertificateService.GetStoreCertificate(thumbprint, storeName, storeLocation); + var certificate = this._sut.CertificateService.GetStoreCertificate(thumbprint, storeName, storeLocation, validOnly: false); // Assert Assert.Null(certificate); } /// - /// Tests that + /// Tests that /// returns a mocked certificate from the certificate store. /// [Fact] @@ -98,7 +98,7 @@ public void GetConfiguredCertificate() .Returns(certificates); // Act - var certificate = this._sut.CertificateService.GetConfiguredCertificate(); + var certificate = this._sut.CertificateService.GetConfiguredCertificate(validOnly: false); // Assert Assert.Multiple(() => @@ -111,7 +111,7 @@ public void GetConfiguredCertificate() } /// - /// Tests that + /// Tests that /// returns if a certificate could not be found in the certificate store. /// [Fact] @@ -135,7 +135,7 @@ public void GetConfiguredCertificate_NotFound() .Returns(new X509Certificate2Collection()); // Act - var certificate = this._sut.CertificateService.GetConfiguredCertificate(); + var certificate = this._sut.CertificateService.GetConfiguredCertificate(validOnly: false); // Assert Assert.Null(certificate); diff --git a/AdvancedSystems.Security/AdvancedSystems.Security.csproj b/AdvancedSystems.Security/AdvancedSystems.Security.csproj index 11b168d..806628a 100644 --- a/AdvancedSystems.Security/AdvancedSystems.Security.csproj +++ b/AdvancedSystems.Security/AdvancedSystems.Security.csproj @@ -11,7 +11,7 @@ - + diff --git a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs index e7e2576..e8cee78 100644 --- a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs +++ b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs @@ -27,23 +27,26 @@ public static partial class CertificateExtensions /// /// The thumbprint of the certificate to locate. /// + /// + /// to allow only valid certificates to be returned from the search; otherwise, . + /// /// /// 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 + public static X509Certificate2 GetCertificate(this T store, string thumbprint, bool validOnly = true) where T : ICertificateStore { store.Open(OpenFlags.ReadOnly); var certificate = store.Certificates - .Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) + .Find(X509FindType.FindByThumbprint, thumbprint, validOnly) .OfType() .FirstOrDefault(); return certificate - ?? throw new CertificateNotFoundException($"""No valid certificate with thumbprint "{thumbprint}" could be found in the store."""); + ?? throw new CertificateNotFoundException($"""No {(validOnly ? "valid " : string.Empty)}certificate with thumbprint "{thumbprint}" could be found in the store."""); } /// diff --git a/AdvancedSystems.Security/Services/CertificateService.cs b/AdvancedSystems.Security/Services/CertificateService.cs index d6adcf8..b48bde1 100644 --- a/AdvancedSystems.Security/Services/CertificateService.cs +++ b/AdvancedSystems.Security/Services/CertificateService.cs @@ -29,12 +29,12 @@ public CertificateService(ILogger logger, IOptions - public X509Certificate2? GetStoreCertificate(string thumbprint, StoreName storeName, StoreLocation storeLocation) + public X509Certificate2? GetStoreCertificate(string thumbprint, StoreName storeName, StoreLocation storeLocation, bool validOnly = true) { try { using var _ = this._logger.BeginScope("Searching for {thumbprint} in {storeName} at {storeLocation}", thumbprint, storeName, storeLocation); - return this._certificateStore.GetCertificate(thumbprint); + return this._certificateStore.GetCertificate(thumbprint, validOnly); } catch (CertificateNotFoundException exception) when (True(() => this._logger.LogError(exception, "{Service} failed to retrieve certificate.", nameof(CertificateService)))) { @@ -43,7 +43,7 @@ public CertificateService(ILogger logger, IOptions - public X509Certificate2? GetConfiguredCertificate() + public X509Certificate2? GetConfiguredCertificate(bool validOnly = true) { var options = this._certificateOptions.Value; @@ -52,7 +52,7 @@ public CertificateService(ILogger logger, IOptions Date: Wed, 8 Jan 2025 22:33:14 +0100 Subject: [PATCH 06/41] Import certificate on Windows via powershell --- .github/workflows/dotnet-tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index bc119a1..0038995 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -34,7 +34,10 @@ jobs: - name: Import Test Certificate (Windows) if: runner.os == 'Windows' run: | - certutil -addstore -f "ROOT" test.cer + $AppSettings = Get-Content '.\AdvancedSystems.Security.Tests\appsettings.json' -Raw | ConvertFrom-Json + $StoreSettings = $AppSettings.Certificate.Store + Import-Certificate -FilePath .\test.cer -CertStoreLocation "Cert:\$($StoreSettings.Location)\$($StoreSettings.Name)" + shell: powershell - name: Import Test Certificate (Ubuntu) if: runner.os == 'Linux' From c4ecd1b756bc53188d07c219beb75a3782eca383 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 11 Jan 2025 18:25:13 +0100 Subject: [PATCH 07/41] Rename CertificateExtensions to CertificateStoreExtensions --- .../Extensions/CertificateExtensionsTests.cs | 10 +++++----- ...cateExtensions.cs => CertificateStoreExtensions.cs} | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename AdvancedSystems.Security/Extensions/{CertificateExtensions.cs => CertificateStoreExtensions.cs} (99%) diff --git a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs index 65eacdb..ed36967 100644 --- a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs @@ -6,14 +6,14 @@ namespace AdvancedSystems.Security.Tests.Extensions; /// -/// Tests the public methods in . +/// Tests the public methods in . /// public sealed class CertificateExtensionsTests { #region Tests /// - /// Tests that + /// Tests that /// extracts the RDNs from the DN correctly from the string. /// [Fact] @@ -29,7 +29,7 @@ public void TestTryParseDistinguishedName() string distinguiedName = $"CN={commonName}, OU={organizationalUnit}, O={organization}, L={locality}, S={state}, C={country}"; // Act - bool isDn = CertificateExtensions.TryParseDistinguishedName(distinguiedName, out DistinguishedName? dn); + bool isDn = CertificateStoreExtensions.TryParseDistinguishedName(distinguiedName, out DistinguishedName? dn); // Assert Assert.Multiple(() => @@ -46,7 +46,7 @@ public void TestTryParseDistinguishedName() } /// - /// Tests that + /// Tests that /// when the DN is malformed. /// [Fact] @@ -56,7 +56,7 @@ public void TestTryParseDistinguishedName_ReturnsNull() string distinguishedName = string.Empty; // Act - bool isDn = CertificateExtensions.TryParseDistinguishedName(distinguishedName, out DistinguishedName? dn); + bool isDn = CertificateStoreExtensions.TryParseDistinguishedName(distinguishedName, out DistinguishedName? dn); // Assert Assert.False(isDn); diff --git a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs b/AdvancedSystems.Security/Extensions/CertificateStoreExtensions.cs similarity index 99% rename from AdvancedSystems.Security/Extensions/CertificateExtensions.cs rename to AdvancedSystems.Security/Extensions/CertificateStoreExtensions.cs index e8cee78..8e9e261 100644 --- a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs +++ b/AdvancedSystems.Security/Extensions/CertificateStoreExtensions.cs @@ -13,7 +13,7 @@ namespace AdvancedSystems.Security.Extensions; /// Defines functions for interacting with X.509 certificates. /// /// -public static partial class CertificateExtensions +public static partial class CertificateStoreExtensions { /// /// Retrieves an X.509 certificate from the specified store using the provided thumbprint. From 84b670279088c14a9de0e5c463ab7de2f65780cb Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 11 Jan 2025 18:43:16 +0100 Subject: [PATCH 08/41] Use certificate-tool for importing certificates in Linux --- .config/dotnet-tools.json | 7 +++++++ .github/workflows/dotnet-tests.yml | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c3dece2..e2a98a3 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -15,6 +15,13 @@ "husky" ], "rollForward": false + }, + "dotnet-certificate-tool": { + "version": "2.0.9", + "commands": [ + "certificate-tool" + ], + "rollForward": false } } } \ No newline at end of file diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index 0038995..9c1771f 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -28,6 +28,9 @@ jobs: - name: Restore Dependencies run: dotnet restore + - name: Restore Dotnet Tools + run: dotnet tool restore + - name: Build run: dotnet build --no-restore @@ -42,8 +45,10 @@ jobs: - name: Import Test Certificate (Ubuntu) if: runner.os == 'Linux' run: | - sudo cp test.cer /usr/local/share/ca-certificates - sudo update-ca-certificates + appSettings='./AdvancedSystems.Security.Tests/appsettings.json' + name=$(jq -r '.Certificate.Store.Name' $appSettings) + location=$(jq -r '.Certificate.Store.Location' $appSettings) + dotnet certificate-tool add -f ./test.cer --store-name $name --store-location $location - name: Import Test Certificate (MacOS) if: runner.os == 'macOS' From 749cc2572253c685632cca21ff74373d2d4537f8 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 11 Jan 2025 18:49:08 +0100 Subject: [PATCH 09/41] Use certificate-tool for importing certificates in MacOS --- .github/workflows/dotnet-tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index 9c1771f..4b0505b 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -53,7 +53,10 @@ jobs: - name: Import Test Certificate (MacOS) if: runner.os == 'macOS' run: | - sudo security add-trusted-cert -d -r trustAsRoot -k /Library/Keychains/System.keychain test.cer + appSettings='./AdvancedSystems.Security.Tests/appsettings.json' + name=$(jq -r '.Certificate.Store.Name' $appSettings) + location=$(jq -r '.Certificate.Store.Location' $appSettings) + dotnet certificate-tool add -f ./test.cer --store-name $name --store-location $location - name: Test run: dotnet test --no-build --verbosity normal From 12f20270c9cd125808a9d01892a8bb0deda4c364 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 11 Jan 2025 19:44:06 +0100 Subject: [PATCH 10/41] Refactor certificate code and start extending implementation of CertificateService --- .../ICertificateService.cs | 1 + .../ServiceCollectionExtensionsTests.cs | 16 ++-- .../Extensions/CertificateExtensionsTests.cs | 10 +-- .../Fixtures/CertificateFixture.cs | 7 +- .../Services/CertificateServiceTests.cs | 69 ---------------- ...Extensions.cs => CertificateExtensions.cs} | 39 +-------- .../Services/CertificateService.cs | 80 ++++++++++++++----- .../Validators/CertificateOptionsValidator.cs | 14 ++-- 8 files changed, 89 insertions(+), 147 deletions(-) rename AdvancedSystems.Security/Extensions/{CertificateStoreExtensions.cs => CertificateExtensions.cs} (77%) diff --git a/AdvancedSystems.Security.Abstractions/ICertificateService.cs b/AdvancedSystems.Security.Abstractions/ICertificateService.cs index 06dc013..7e974e3 100644 --- a/AdvancedSystems.Security.Abstractions/ICertificateService.cs +++ b/AdvancedSystems.Security.Abstractions/ICertificateService.cs @@ -7,6 +7,7 @@ namespace AdvancedSystems.Security.Abstractions; /// /// Defines a service for managing and retrieving X.509 certificates. /// +/// public interface ICertificateService { #region Methods diff --git a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index 6c238f0..f713392 100644 --- a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -24,12 +24,15 @@ public sealed class ServiceCollectionExtensionsTests #region AddCertificateService Tests /// - /// Tests that can be initialized through dependency injection from configuration options. + /// Tests that can be initialized through dependency injection + /// from configuration options. /// [Fact] public async Task TestAddCertificateService_FromOptions() { // Arrange + string thumbprint = "A24421E3B4149A12B219AA67CD263D419829BD53"; + using var hostBuilder = await new HostBuilder() .ConfigureWebHost(builder => builder .UseTestServer() @@ -37,7 +40,7 @@ public async Task TestAddCertificateService_FromOptions() { services.AddCertificateService(options => { - options.Thumbprint = "A24421E3B4149A12B219AA67CD263D419829BD53"; + options.Thumbprint = thumbprint; options.Store = new CertificateStoreOptions { Location = StoreLocation.CurrentUser, @@ -53,7 +56,7 @@ public async Task TestAddCertificateService_FromOptions() // Act var certificateService = hostBuilder.Services.GetService(); - var certificate = certificateService?.GetConfiguredCertificate(validOnly: false); + var certificate = certificateService?.GetStoreCertificate(thumbprint, StoreName.My, StoreLocation.CurrentUser, validOnly: false); // Assert Assert.Multiple(() => @@ -66,12 +69,15 @@ public async Task TestAddCertificateService_FromOptions() } /// - /// Tests that can be initialized through dependency injection from configuration sections. + /// Tests that can be initialized through dependency injection + /// from configuration sections. /// [Fact] public async Task TestAddCertificateService_FromAppSettings() { // Arrange + string thumbprint = "A24421E3B4149A12B219AA67CD263D419829BD53"; + using var hostBuilder = await new HostBuilder() .ConfigureWebHost(builder => builder .UseTestServer() @@ -91,7 +97,7 @@ public async Task TestAddCertificateService_FromAppSettings() // Act var certificateService = hostBuilder.Services.GetService(); - var certificate = certificateService?.GetConfiguredCertificate(validOnly: false); + var certificate = certificateService?.GetStoreCertificate(thumbprint, StoreName.My, StoreLocation.CurrentUser, validOnly: false); // Assert Assert.Multiple(() => diff --git a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs index ed36967..65eacdb 100644 --- a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs @@ -6,14 +6,14 @@ namespace AdvancedSystems.Security.Tests.Extensions; /// -/// Tests the public methods in . +/// Tests the public methods in . /// public sealed class CertificateExtensionsTests { #region Tests /// - /// Tests that + /// Tests that /// extracts the RDNs from the DN correctly from the string. /// [Fact] @@ -29,7 +29,7 @@ public void TestTryParseDistinguishedName() string distinguiedName = $"CN={commonName}, OU={organizationalUnit}, O={organization}, L={locality}, S={state}, C={country}"; // Act - bool isDn = CertificateStoreExtensions.TryParseDistinguishedName(distinguiedName, out DistinguishedName? dn); + bool isDn = CertificateExtensions.TryParseDistinguishedName(distinguiedName, out DistinguishedName? dn); // Assert Assert.Multiple(() => @@ -46,7 +46,7 @@ public void TestTryParseDistinguishedName() } /// - /// Tests that + /// Tests that /// when the DN is malformed. /// [Fact] @@ -56,7 +56,7 @@ public void TestTryParseDistinguishedName_ReturnsNull() string distinguishedName = string.Empty; // Act - bool isDn = CertificateStoreExtensions.TryParseDistinguishedName(distinguishedName, out DistinguishedName? dn); + bool isDn = CertificateExtensions.TryParseDistinguishedName(distinguishedName, out DistinguishedName? dn); // Assert Assert.False(isDn); diff --git a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs index 5fe4e6f..a525727 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs @@ -4,11 +4,9 @@ using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.Options; using AdvancedSystems.Security.Services; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Moq; @@ -19,9 +17,8 @@ public class CertificateFixture public CertificateFixture() { this.Logger = new Mock>(); - this.Options = new Mock>(); this.Store = new Mock(); - this.CertificateService = new CertificateService(this.Logger.Object, this.Options.Object, this.Store.Object); + this.CertificateService = new CertificateService(this.Logger.Object, this.Store.Object); } #region Properties @@ -30,8 +27,6 @@ public CertificateFixture() public ICertificateService CertificateService { get; private set; } - public Mock> Options { get; private set; } - public Mock Store { get; private set; } #endregion diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs index ea3a407..5cf262c 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs @@ -72,74 +72,5 @@ public void TestGetStoreCertificate_NotFound() Assert.Null(certificate); } - /// - /// Tests that - /// returns a mocked certificate from the certificate store. - /// - [Fact] - public void GetConfiguredCertificate() - { - // Arrange - var certificates = CertificateFixture.CreateCertificateCollection(3); - var certificateOptions = new CertificateOptions - { - Thumbprint = certificates.Select(x => x.Thumbprint).First(), - Store = new CertificateStoreOptions - { - Location = StoreLocation.CurrentUser, - Name = StoreName.My, - } - }; - - this._sut.Options.Setup(x => x.Value) - .Returns(certificateOptions); - - this._sut.Store.Setup(x => x.Certificates) - .Returns(certificates); - - // Act - var certificate = this._sut.CertificateService.GetConfiguredCertificate(validOnly: false); - - // Assert - 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() - { - // Arrange - var certificateOptions = new CertificateOptions - { - Thumbprint = "123456789", - Store = new CertificateStoreOptions - { - Location = StoreLocation.CurrentUser, - Name = StoreName.My, - } - }; - - this._sut.Options.Setup(x => x.Value) - .Returns(certificateOptions); - - this._sut.Store.Setup(x => x.Certificates) - .Returns(new X509Certificate2Collection()); - - // Act - var certificate = this._sut.CertificateService.GetConfiguredCertificate(validOnly: false); - - // Assert - Assert.Null(certificate); - } - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security/Extensions/CertificateStoreExtensions.cs b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs similarity index 77% rename from AdvancedSystems.Security/Extensions/CertificateStoreExtensions.cs rename to AdvancedSystems.Security/Extensions/CertificateExtensions.cs index 8e9e261..ae9888a 100644 --- a/AdvancedSystems.Security/Extensions/CertificateStoreExtensions.cs +++ b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs @@ -1,10 +1,7 @@ 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; namespace AdvancedSystems.Security.Extensions; @@ -13,42 +10,8 @@ namespace AdvancedSystems.Security.Extensions; /// Defines functions for interacting with X.509 certificates. /// /// -public static partial class CertificateStoreExtensions +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. - /// - /// - /// to allow only valid certificates to be returned from the search; otherwise, . - /// - /// - /// 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, bool validOnly = true) where T : ICertificateStore - { - store.Open(OpenFlags.ReadOnly); - - var certificate = store.Certificates - .Find(X509FindType.FindByThumbprint, thumbprint, validOnly) - .OfType() - .FirstOrDefault(); - - return certificate - ?? throw new CertificateNotFoundException($"""No {(validOnly ? "valid " : string.Empty)}certificate with thumbprint "{thumbprint}" could be found in the store."""); - } - /// /// Attempts to parse the specified distinguished name (DN) string into a object. /// diff --git a/AdvancedSystems.Security/Services/CertificateService.cs b/AdvancedSystems.Security/Services/CertificateService.cs index b48bde1..2441efd 100644 --- a/AdvancedSystems.Security/Services/CertificateService.cs +++ b/AdvancedSystems.Security/Services/CertificateService.cs @@ -1,14 +1,12 @@ -using System.Security.Cryptography.X509Certificates; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.Abstractions.Exceptions; -using AdvancedSystems.Security.Extensions; -using AdvancedSystems.Security.Options; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using static AdvancedSystems.Core.Common.ExceptionFilter; namespace AdvancedSystems.Security.Services; @@ -16,43 +14,87 @@ namespace AdvancedSystems.Security.Services; public sealed class CertificateService : ICertificateService { private readonly ILogger _logger; - private readonly IOptions _certificateOptions; private readonly ICertificateStore _certificateStore; - public CertificateService(ILogger logger, IOptions certificateOptions, ICertificateStore certificateStore) + public CertificateService(ILogger logger, ICertificateStore certificateStore) { this._logger = logger; - this._certificateOptions = certificateOptions; this._certificateStore = certificateStore; } #region Methods + public void ImportCertificate() + { + throw new NotImplementedException(); + } + + public IEnumerable GetCertificate() + { + try + { + this._certificateStore.Open(OpenFlags.ReadOnly); + + return this._certificateStore.Certificates.OfType(); + } + catch (ArgumentNullException) + { + return Enumerable.Empty(); + } + finally + { + this._certificateStore.Close(); + } + } + /// public X509Certificate2? GetStoreCertificate(string thumbprint, StoreName storeName, StoreLocation storeLocation, bool validOnly = true) { try { - using var _ = this._logger.BeginScope("Searching for {thumbprint} in {storeName} at {storeLocation}", thumbprint, storeName, storeLocation); - return this._certificateStore.GetCertificate(thumbprint, validOnly); + this._certificateStore.Open(OpenFlags.ReadOnly); + + var certificate = this._certificateStore.Certificates + .Find(X509FindType.FindByThumbprint, thumbprint, validOnly) + .OfType() + .FirstOrDefault(); + + return certificate; } - catch (CertificateNotFoundException exception) when (True(() => this._logger.LogError(exception, "{Service} failed to retrieve certificate.", nameof(CertificateService)))) + finally { - return null; + this._certificateStore.Close(); } } /// public X509Certificate2? GetConfiguredCertificate(bool validOnly = true) { - var options = this._certificateOptions.Value; + return null; + } - if (string.IsNullOrEmpty(options.Thumbprint) || options?.Store is null) + public bool RemoveCertificate(string thumbprint) + { + try { - return null; - } + this._certificateStore.Open(OpenFlags.ReadWrite); - return this.GetStoreCertificate(options.Thumbprint, options.Store.Name, options.Store.Location, validOnly); + X509Certificate2Collection? certificates = this._certificateStore.Certificates + .Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false); + + if (certificates is null) return false; + + this._certificateStore.RemoveRange(certificates); + return true; + } + catch (CryptographicException) + { + return false; + } + finally + { + this._certificateStore.Close(); + } } #endregion diff --git a/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs b/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs index d4d60f2..1240a82 100644 --- a/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs +++ b/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs @@ -2,7 +2,6 @@ using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Abstractions.Exceptions; -using AdvancedSystems.Security.Extensions; using AdvancedSystems.Security.Options; using Microsoft.Extensions.Logging; @@ -13,12 +12,12 @@ namespace AdvancedSystems.Security.Validators; public sealed class CertificateOptionsValidator : IValidateOptions { private readonly ILogger _logger; - private readonly ICertificateStore _certificateStore; + private readonly ICertificateService _certificateService; - public CertificateOptionsValidator(ILogger logger, ICertificateStore certificateStore) + public CertificateOptionsValidator(ILogger logger, ICertificateService certificateService) { this._logger = logger; - this._certificateStore = certificateStore; + this._certificateService = certificateService; } #region Implementation @@ -34,7 +33,12 @@ public ValidateOptionsResult Validate(string? name, CertificateOptions options) try { - X509Certificate2 certificate = this._certificateStore.GetCertificate(options.Thumbprint, validOnly: false); + X509Certificate2? certificate = this._certificateService.GetConfiguredCertificate(validOnly: false); + + if (certificate is null) + { + return ValidateOptionsResult.Fail($"Configured certificate with thumbprint \"{options.Thumbprint}\" could not be found."); + } } catch (CertificateNotFoundException exception) { From bfcebfd8759e6a2017a53ecbc11a0f7ea00cf0c7 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 12 Jan 2025 19:38:31 +0100 Subject: [PATCH 11/41] Rewrite CertificateService and DI logic from scratch and extand interface --- .../ICertificateService.cs | 56 ++-- .../ServiceCollectionExtensionsTests.cs | 71 ++--- .../Fixtures/CertificateFixture.cs | 64 ++--- .../Services/CertificateServiceTests.cs | 48 +--- .../appsettings.json | 9 +- .../AdvancedSystems.Security.csproj | 5 +- .../DependencyInjection/Sections.cs | 9 +- .../ServiceCollectionExtensions.cs | 80 ++---- .../Options/CertificateOptions.cs | 10 - .../Options/RSACryptoOptions.cs | 3 + .../Services/CertificateService.cs | 247 ++++++++++++++++-- .../Services/RSACryptoService.cs | 26 +- .../Validators/CertificateOptionsValidator.cs | 93 ++++--- 13 files changed, 399 insertions(+), 322 deletions(-) delete mode 100644 AdvancedSystems.Security/Options/CertificateOptions.cs diff --git a/AdvancedSystems.Security.Abstractions/ICertificateService.cs b/AdvancedSystems.Security.Abstractions/ICertificateService.cs index 7e974e3..811c703 100644 --- a/AdvancedSystems.Security.Abstractions/ICertificateService.cs +++ b/AdvancedSystems.Security.Abstractions/ICertificateService.cs @@ -1,6 +1,6 @@ -using System.Security.Cryptography.X509Certificates; - -using AdvancedSystems.Security.Abstractions.Exceptions; +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; namespace AdvancedSystems.Security.Abstractions; @@ -12,41 +12,21 @@ public interface ICertificateService { #region Methods - /// - /// Retrieves an X.509 certificate from the specified store using the provided - /// . - /// - /// - /// The thumbprint of the certificate to locate. - /// - /// - /// The certificate store from which to retrieve the certificate. - /// - /// - /// The location of the certificate store, such as - /// or . - /// - /// - /// to allow only valid certificates to be returned from the search; otherwise, . - /// - /// - /// The object if the certificate is found, else null. - /// - /// - /// Thrown when no certificate with the specified thumbprint is found in the store. - /// - X509Certificate2? GetStoreCertificate(string thumbprint, StoreName storeName, StoreLocation storeLocation, bool validOnly = true); - - /// - /// Retrieves an application-configured X.509 certificate. - /// - /// - /// to allow only valid certificates to be returned from the search; otherwise, . - /// - /// - /// The object if the certificate is found, else null. - /// - X509Certificate2? GetConfiguredCertificate(bool validOnly = true); + bool AddCertificate(string storeService, X509Certificate2 certificate); + + bool TryImportPemCertificate(string storeService, string publicKeyPath, string privateKeyPath, out X509Certificate2? certificate); + + bool TryImportPemCertificate(string storeService, string publicKeyPath, string privateKeyPath, string password, out X509Certificate2? certificate); + + bool TryImportPfxCertificate(string storeService, string path, string password, out X509Certificate2? certificate); + + bool TryImportPfxCertificate(string storeService, string path, out X509Certificate2? certificate); + + IEnumerable GetCertificate(string storeService); + + X509Certificate2? GetCertificate(string storeService, string thumbprint, bool validOnly = true); + + bool RemoveCertificate(string storeService, string thumbprint); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index f713392..4301c0f 100644 --- a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -4,7 +4,6 @@ using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.DependencyInjection; -using AdvancedSystems.Security.Options; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; @@ -24,13 +23,13 @@ public sealed class ServiceCollectionExtensionsTests #region AddCertificateService Tests /// - /// Tests that can be initialized through dependency injection - /// from configuration options. + /// Tests that can be initialized through dependency injection. /// [Fact] public async Task TestAddCertificateService_FromOptions() { // Arrange + string storeService = "my/CurrentUser"; string thumbprint = "A24421E3B4149A12B219AA67CD263D419829BD53"; using var hostBuilder = await new HostBuilder() @@ -38,56 +37,13 @@ public async Task TestAddCertificateService_FromOptions() .UseTestServer() .ConfigureServices(services => { - services.AddCertificateService(options => + services.AddCertificateStore(storeService, options => { - options.Thumbprint = thumbprint; - options.Store = new CertificateStoreOptions - { - Location = StoreLocation.CurrentUser, - Name = StoreName.My, - }; + options.Location = StoreLocation.CurrentUser; + options.Name = StoreName.My; }); - }) - .Configure(app => - { - - })) - .StartAsync(); - - // Act - var certificateService = hostBuilder.Services.GetService(); - var certificate = certificateService?.GetStoreCertificate(thumbprint, StoreName.My, StoreLocation.CurrentUser, validOnly: false); - - // Assert - Assert.Multiple(() => - { - Assert.NotNull(certificateService); - Assert.NotNull(certificate); - }); - - await hostBuilder.StopAsync(); - } - - /// - /// Tests that can be initialized through dependency injection - /// from configuration sections. - /// - [Fact] - public async Task TestAddCertificateService_FromAppSettings() - { - // Arrange - string thumbprint = "A24421E3B4149A12B219AA67CD263D419829BD53"; - 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)); + services.AddCertificateService(); }) .Configure(app => { @@ -97,7 +53,7 @@ public async Task TestAddCertificateService_FromAppSettings() // Act var certificateService = hostBuilder.Services.GetService(); - var certificate = certificateService?.GetStoreCertificate(thumbprint, StoreName.My, StoreLocation.CurrentUser, validOnly: false); + var certificate = certificateService?.GetCertificate(storeService, thumbprint, validOnly: false); // Assert Assert.Multiple(() => @@ -120,12 +76,14 @@ public async Task TestAddCertificateService_FromAppSettings() public async Task TestAddCertificateStore_FromOptions() { // Arrange + string storeService = "my/CurrentUser"; + using var hostBuilder = await new HostBuilder() .ConfigureWebHost(builder => builder .UseTestServer() .ConfigureServices(services => { - services.AddCertificateStore(options => + services.AddCertificateStore(storeService, options => { options.Name = StoreName.My; options.Location = StoreLocation.CurrentUser; @@ -138,7 +96,7 @@ public async Task TestAddCertificateStore_FromOptions() .StartAsync(); // Act - var certificateStore = hostBuilder.Services.GetService(); + var certificateStore = hostBuilder.Services.GetKeyedService(storeService); // Assert Assert.NotNull(certificateStore); @@ -152,6 +110,8 @@ public async Task TestAddCertificateStore_FromOptions() public async Task TestAddCertificateStore_FromAppSettings() { // Arrange + string storeService = "my/CurrentUser"; + using var hostBuilder = await new HostBuilder() .ConfigureWebHost(builder => builder .UseTestServer() @@ -161,7 +121,8 @@ public async Task TestAddCertificateStore_FromAppSettings() }) .ConfigureServices((context, services) => { - services.AddCertificateStore(context.Configuration.GetSection($"{Sections.CERTIFICATE}:{Sections.STORE}")); + var storeSettings = context.Configuration.GetSection(Sections.CERTIFICATE_STORE); + services.AddCertificateStore(storeService, storeSettings); }) .Configure(app => { @@ -170,7 +131,7 @@ public async Task TestAddCertificateStore_FromAppSettings() .StartAsync(); // Act - var certificateStore = hostBuilder.Services.GetService(); + var certificateStore = hostBuilder.Services.GetKeyedService(storeService); // Assert Assert.NotNull(certificateStore); diff --git a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs index a525727..dec7af7 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs @@ -1,53 +1,57 @@ -using System; -using System.Linq; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; -using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.Services; +using AdvancedSystems.Security.DependencyInjection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Moq; +using Xunit; namespace AdvancedSystems.Security.Tests.Fixtures; -public class CertificateFixture +public class CertificateFixture : IAsyncLifetime { - public CertificateFixture() - { - this.Logger = new Mock>(); - this.Store = new Mock(); - this.CertificateService = new CertificateService(this.Logger.Object, this.Store.Object); - } - #region Properties - public Mock> Logger { get; private set; } - - public ICertificateService CertificateService { get; private set; } + public string ConfiguredStoreService { get; } = "my/CurrentUser"; - public Mock Store { get; private set; } + public IHost? Host { get; private set; } #endregion - #region Helper Methods + #region Methods - public static X509Certificate2 CreateCertificate(string subjectName) + public async Task InitializeAsync() { - using var ecdsa = ECDsa.Create(); - var request = new CertificateRequest(subjectName, ecdsa, HashAlgorithmName.SHA256); - var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddHours(1)); - return certificate; + this.Host = await new HostBuilder() + .ConfigureWebHost(builder => builder + .UseTestServer() + .ConfigureAppConfiguration(config => + { + config.AddJsonFile("appsettings.json", optional: false); + }) + .ConfigureServices((context, services) => + { + var storeSettings = context.Configuration.GetSection(Sections.CERTIFICATE_STORE); + services.AddCertificateStore(this.ConfiguredStoreService, storeSettings); + services.AddCertificateService(); + }) + .Configure(app => + { + + })) + .StartAsync(); } - public static X509Certificate2Collection CreateCertificateCollection(int length) + public async Task DisposeAsync() { - var certificates = Enumerable.Range(0, length) - .Select(_ => CreateCertificate("O=AdvancedSystems")) - .ToArray(); + if (this.Host is null) return; - return new X509Certificate2Collection(certificates); + await this.Host.StopAsync(); } #endregion diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs index 5cf262c..62ea42d 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs @@ -1,11 +1,9 @@ -using System.Linq; -using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.Options; using AdvancedSystems.Security.Tests.Fixtures; -using Moq; +using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -18,28 +16,27 @@ public sealed class CertificateServiceTests : IClassFixture { private readonly CertificateFixture _sut; - public CertificateServiceTests(CertificateFixture fixture) + public CertificateServiceTests(CertificateFixture certificateFixture) { - this._sut = fixture; + this._sut = certificateFixture; } #region Tests /// - /// Tests that - /// returns a mocked certificate from the certificate store. + /// Tests that + /// returns a certificate from the certificate store. /// [Fact] - public void TestGetStoreCertificate() + public void TestGetCertificate_ByThumbprint() { // Arrange - var certificates = CertificateFixture.CreateCertificateCollection(3); - string thumbprint = certificates.Select(x => x.Thumbprint).First(); - this._sut.Store.Setup(x => x.Certificates) - .Returns(certificates); + string storeService = this._sut.ConfiguredStoreService; + string thumbprint = "A24421E3B4149A12B219AA67CD263D419829BD53"; // Act - var certificate = this._sut.CertificateService.GetStoreCertificate(thumbprint, StoreName.My, StoreLocation.CurrentUser, validOnly: false); + var certificateService = this._sut.Host?.Services.GetService(); + X509Certificate2? certificate = certificateService?.GetCertificate(storeService, thumbprint, validOnly: false); // Assert Assert.Multiple(() => @@ -47,29 +44,6 @@ public void TestGetStoreCertificate() 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() - { - // Arrange - string thumbprint = "123456789"; - var storeName = StoreName.My; - var storeLocation = StoreLocation.CurrentUser; - this._sut.Store.Setup(x => x.Certificates) - .Returns(new X509Certificate2Collection()); - - // Act - var certificate = this._sut.CertificateService.GetStoreCertificate(thumbprint, storeName, storeLocation, validOnly: false); - - // Assert - Assert.Null(certificate); } #endregion diff --git a/AdvancedSystems.Security.Tests/appsettings.json b/AdvancedSystems.Security.Tests/appsettings.json index 844840d..725280e 100644 --- a/AdvancedSystems.Security.Tests/appsettings.json +++ b/AdvancedSystems.Security.Tests/appsettings.json @@ -1,9 +1,6 @@ { - "Certificate": { - "Thumbprint": "A24421E3B4149A12B219AA67CD263D419829BD53", - "Store": { - "Location": "CurrentUser", - "Name": "My" - } + "CertificateStore": { + "Location": "CurrentUser", + "Name": "My" } } \ No newline at end of file diff --git a/AdvancedSystems.Security/AdvancedSystems.Security.csproj b/AdvancedSystems.Security/AdvancedSystems.Security.csproj index 806628a..60a8bdc 100644 --- a/AdvancedSystems.Security/AdvancedSystems.Security.csproj +++ b/AdvancedSystems.Security/AdvancedSystems.Security.csproj @@ -11,7 +11,6 @@ - @@ -20,4 +19,8 @@ + + + + diff --git a/AdvancedSystems.Security/DependencyInjection/Sections.cs b/AdvancedSystems.Security/DependencyInjection/Sections.cs index 21a9db9..f08e1d5 100644 --- a/AdvancedSystems.Security/DependencyInjection/Sections.cs +++ b/AdvancedSystems.Security/DependencyInjection/Sections.cs @@ -7,18 +7,13 @@ namespace AdvancedSystems.Security.DependencyInjection; /// 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. + /// Key used to bind the configuration section. /// - public const string STORE = "Store"; + public const string CERTIFICATE_STORE = "CertificateStore"; } \ No newline at end of file diff --git a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs index 7443ff7..11e3c9a 100644 --- a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,5 +1,5 @@ using System; -using System.Security.Cryptography.X509Certificates; +using System.Runtime.CompilerServices; using AdvancedSystems.Core.DependencyInjection; using AdvancedSystems.Security.Abstractions; @@ -11,7 +11,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using Microsoft.VisualBasic.FileIO; namespace AdvancedSystems.Security.DependencyInjection; @@ -57,11 +56,9 @@ public static IServiceCollection AddHashService(this IServiceCollection services #region CertificateStore - private record CertificateOptionsCarrier(StoreName StoreName, StoreLocation StoreLocation); - - private static IServiceCollection AddCertificateStore(this IServiceCollection services) + private static IServiceCollection AddCertificateStore(this IServiceCollection services, string key) { - services.TryAdd(ServiceDescriptor.Singleton(serviceProvider => + services.TryAdd(ServiceDescriptor.KeyedSingleton(key, (serviceProvider, _) => { var options = serviceProvider.GetRequiredService>().Value; return new CertificateStore(options.Name, options.Location); @@ -76,18 +73,21 @@ private static IServiceCollection AddCertificateStore(this IServiceCollection se /// /// The service collection containing the service. /// + /// + /// Name of the keyed service. + /// /// /// An action used to configure . /// /// /// The value of . /// - public static IServiceCollection AddCertificateStore(this IServiceCollection services, Action setupAction) + public static IServiceCollection AddCertificateStore(this IServiceCollection services, string key, Action setupAction) { services.AddOptions() .Configure(setupAction); - services.AddCertificateStore(); + services.AddCertificateStore(key); return services; } @@ -97,16 +97,19 @@ public static IServiceCollection AddCertificateStore(this IServiceCollection ser /// /// The service collection containing the service. /// + /// + /// Name of the keyed service. + /// /// /// A configuration section targeting . /// /// /// The value of . /// - public static IServiceCollection AddCertificateStore(this IServiceCollection services, IConfigurationSection configurationSection) + public static IServiceCollection AddCertificateStore(this IServiceCollection services, string key, IConfigurationSection configurationSection) { services.TryAddOptions(configurationSection); - services.AddCertificateStore(); + services.AddCertificateStore(key); return services; } @@ -114,67 +117,18 @@ public static IServiceCollection AddCertificateStore(this IServiceCollection ser #region CertificateService - private static IServiceCollection AddCertificateService(this IServiceCollection services) - { - services.TryAdd(ServiceDescriptor.Scoped()); - services.TryAdd(ServiceDescriptor.Singleton, CertificateOptionsValidator>()); - - return services; - } - /// /// 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) + public static IServiceCollection AddCertificateService(this IServiceCollection services) { - services.AddOptions() - .Configure(setupAction); - - services.Configure(options => - { - var certificateOptions = new CertificateOptions(); - setupAction.Invoke(certificateOptions); - - var store = certificateOptions.Store - ?? throw new ArgumentNullException(nameof(setupAction), $"{nameof(CertificateStoreOptions)} settings are undefined."); - - options.Name = store.Name; - options.Location = store.Location; - }); - - services.AddCertificateStore(); - services.AddCertificateService(); - - return services; - } - - /// - /// 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.TryAddOptions(configurationSection); - services.AddCertificateStore(configurationSection.GetRequiredSection(Sections.STORE)); - services.AddCertificateService(); + services.TryAdd(ServiceDescriptor.Scoped()); return services; } @@ -202,6 +156,8 @@ private static IServiceCollection AddRSACryptoService(this IServiceCollection se /// public static IServiceCollection AddRSACryptoService(this IServiceCollection services, Action setupAction) { + services.AddRSACryptoService(); + throw new NotImplementedException(); } @@ -219,6 +175,8 @@ public static IServiceCollection AddRSACryptoService(this IServiceCollection ser /// public static IServiceCollection AddRSACryptoService(this IServiceCollection services, IConfigurationSection configurationSection) { + services.AddRSACryptoService(); + throw new NotImplementedException(); } diff --git a/AdvancedSystems.Security/Options/CertificateOptions.cs b/AdvancedSystems.Security/Options/CertificateOptions.cs deleted file mode 100644 index 088462e..0000000 --- a/AdvancedSystems.Security/Options/CertificateOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace AdvancedSystems.Security.Options; - -public sealed record CertificateOptions -{ - public string? Thumbprint { get; set; } - - public CertificateStoreOptions? Store { get; set; } -} \ No newline at end of file diff --git a/AdvancedSystems.Security/Options/RSACryptoOptions.cs b/AdvancedSystems.Security/Options/RSACryptoOptions.cs index 77b3975..6cbea4a 100644 --- a/AdvancedSystems.Security/Options/RSACryptoOptions.cs +++ b/AdvancedSystems.Security/Options/RSACryptoOptions.cs @@ -17,4 +17,7 @@ public sealed record RSACryptoOptions [Required] public required Encoding Encoding { get; set; } + + [Required] + public required string Thumbprint { get; set; } } \ No newline at end of file diff --git a/AdvancedSystems.Security/Services/CertificateService.cs b/AdvancedSystems.Security/Services/CertificateService.cs index 2441efd..9deac68 100644 --- a/AdvancedSystems.Security/Services/CertificateService.cs +++ b/AdvancedSystems.Security/Services/CertificateService.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Security.Abstractions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace AdvancedSystems.Security.Services; @@ -14,28 +17,197 @@ namespace AdvancedSystems.Security.Services; public sealed class CertificateService : ICertificateService { private readonly ILogger _logger; - private readonly ICertificateStore _certificateStore; + private readonly IServiceProvider _serviceProvider; - public CertificateService(ILogger logger, ICertificateStore certificateStore) + public CertificateService(ILogger logger, IServiceProvider serviceProvider) { this._logger = logger; - this._certificateStore = certificateStore; + this._serviceProvider = serviceProvider; } #region Methods - public void ImportCertificate() + /// + public bool AddCertificate(string storeService, X509Certificate2 certificate) + { + var store = this._serviceProvider.GetRequiredKeyedService(storeService); + + try + { + store.Open(OpenFlags.ReadWrite); + store.Add(certificate); + return true; + } + catch (Exception) + { + this._logger.LogError( + "Failed to remove certificate (\"{Thumbprint}\") from store (L='{Location}',N='{Name}').", + certificate.Thumbprint, + store.Location, + store.Name + ); + + return false; + } + finally + { + store.Close(); + } + } + + /// + public bool TryImportPemCertificate(string storeService, string publicKeyPath, string? privateKeyPath, [NotNullWhen(true)] out X509Certificate2? certificate) + { + return this.TryImportPemCertificate(storeService, publicKeyPath, privateKeyPath, string.Empty, out certificate); + } + + /// + public bool TryImportPemCertificate(string storeService, string publicKeyPath, string? privateKeyPath, string password, [NotNullWhen(true)] out X509Certificate2? certificate) + { + if (!File.Exists(publicKeyPath)) + { + this._logger.LogError( + "Public key file does not exist (PublicKey=\"{PublicKey}\").", + publicKeyPath + ); + + certificate = null; + return false; + } + + try + { + using var publicKey = string.IsNullOrEmpty(password) + ? new X509Certificate2(publicKeyPath) + : new X509Certificate2(publicKeyPath, password, KeyStorageFlags); + + if (!string.IsNullOrEmpty(privateKeyPath)) + { + if (!File.Exists(privateKeyPath)) + { + this._logger.LogError( + "Private key file does not exist (PrivateKey=\"{PrivateKey}\").", + privateKeyPath + ); + + certificate = null; + return false; + } + + string[] privateKeyBlocks = File.ReadAllText(privateKeyPath) + .Split("-", StringSplitOptions.RemoveEmptyEntries); + + string header = privateKeyBlocks[0]; + + byte[] privateKeyBuffer = Convert.FromBase64String(privateKeyBlocks[1]); + using var privateKey = RSA.Create(); + + switch (header) + { + case PKCS8_PRIVATE_KEY_HEADER: + privateKey.ImportPkcs8PrivateKey(privateKeyBuffer, out _); + break; + case PKCS8_ENCRYPTED_PRIVATE_KEY_HEADER: + privateKey.ImportEncryptedPkcs8PrivateKey(password, privateKeyBuffer, out _); + break; + case RSA_PRIVATE_KEY_HEADER: + privateKey.ImportRSAPrivateKey(privateKeyBuffer, out _); + break; + default: + this._logger.LogCritical( + "Unknown header in private key: {Header} (\"{PrivateKey}\").", + header, + privateKeyPath + ); + + certificate = null; + return false; + } + + certificate = publicKey.CopyWithPrivateKey(privateKey); + } + else + { + certificate = publicKey; + } + + bool isImported = this.AddCertificate(storeService, certificate); + return isImported; + } + catch (CryptographicException) + { + if (!string.IsNullOrEmpty(privateKeyPath)) + { + this._logger.LogError( + "Failed to initialize public key or private key from path (PublicKey=\"{PublicKey}\",PrivateKey=\"{PrivateKey}\").", + publicKeyPath, + privateKeyPath + ); + } + else + { + this._logger.LogError( + "Failed to initialize public key from path (PublicKey=\"{PublicKey}\").", + publicKeyPath + ); + } + + certificate = null; + return false; + } + } + + /// + public bool TryImportPfxCertificate(string storeService, string path, [NotNullWhen(true)] out X509Certificate2? certificate) { - throw new NotImplementedException(); + return this.TryImportPfxCertificate(storeService, path, string.Empty, out certificate); + } + + /// + public bool TryImportPfxCertificate(string storeService, string path, string password, [NotNullWhen(true)] out X509Certificate2? certificate) + { + if (!File.Exists(path)) + { + this._logger.LogError( + "Certificate file does not exist (Path=\"{Certificate}\").", + path + ); + + certificate = null; + return false; + } + + try + { + certificate = string.IsNullOrEmpty(password) + ? new X509Certificate2(path) + : new X509Certificate2(path, password, KeyStorageFlags); + + bool isImported = this.AddCertificate(storeService, certificate); + return isImported; + } + catch (CryptographicException) + { + this._logger.LogError( + "Failed to initialize certificate from path (Path=\"{Certificate}\").", + path + ); + + certificate = null; + return false; + } } - public IEnumerable GetCertificate() + /// + public IEnumerable GetCertificate(string storeService) { + var store = this._serviceProvider.GetRequiredKeyedService(storeService); + try { - this._certificateStore.Open(OpenFlags.ReadOnly); + store.Open(OpenFlags.ReadOnly); - return this._certificateStore.Certificates.OfType(); + return store.Certificates.OfType(); } catch (ArgumentNullException) { @@ -43,18 +215,20 @@ public IEnumerable GetCertificate() } finally { - this._certificateStore.Close(); + store.Close(); } } /// - public X509Certificate2? GetStoreCertificate(string thumbprint, StoreName storeName, StoreLocation storeLocation, bool validOnly = true) + public X509Certificate2? GetCertificate(string storeService, string thumbprint, bool validOnly = true) { + var store = this._serviceProvider.GetRequiredKeyedService(storeService); + try { - this._certificateStore.Open(OpenFlags.ReadOnly); + store.Open(OpenFlags.ReadOnly); - var certificate = this._certificateStore.Certificates + var certificate = store.Certificates .Find(X509FindType.FindByThumbprint, thumbprint, validOnly) .OfType() .FirstOrDefault(); @@ -63,37 +237,66 @@ public IEnumerable GetCertificate() } finally { - this._certificateStore.Close(); + store.Close(); } } /// - public X509Certificate2? GetConfiguredCertificate(bool validOnly = true) + public bool RemoveCertificate(string storeService, string thumbprint) { - return null; - } + var store = this._serviceProvider.GetRequiredKeyedService(storeService); - public bool RemoveCertificate(string thumbprint) - { try { - this._certificateStore.Open(OpenFlags.ReadWrite); + store.Open(OpenFlags.ReadWrite); - X509Certificate2Collection? certificates = this._certificateStore.Certificates + X509Certificate2Collection? certificates = store.Certificates .Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false); if (certificates is null) return false; - this._certificateStore.RemoveRange(certificates); + store.RemoveRange(certificates); return true; } catch (CryptographicException) { + this._logger.LogError( + "Failed to remove certificate (\"{Thumbprint}\") from store (L='{Location}',N='{Name}').", + thumbprint, + store.Location, + store.Name + ); + return false; } finally { - this._certificateStore.Close(); + store.Close(); + } + } + + #endregion + + #region Helpers + + private const string PKCS8_PRIVATE_KEY_HEADER = "BEGIN PRIVATE KEY"; + + private const string PKCS8_ENCRYPTED_PRIVATE_KEY_HEADER = "BEGIN ENCRYPTED PRIVATE KEY"; + + private const string RSA_PRIVATE_KEY_HEADER = "BEGIN RSA PRIVATE KEY"; + + private static X509KeyStorageFlags KeyStorageFlags + { + get + { + var keyStorageFlags = X509KeyStorageFlags.DefaultKeySet; + + if (OperatingSystem.IsMacOS()) + { + keyStorageFlags = X509KeyStorageFlags.Exportable; + } + + return keyStorageFlags; } } diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs index 2b1fad3..27ca043 100644 --- a/AdvancedSystems.Security/Services/RSACryptoService.cs +++ b/AdvancedSystems.Security/Services/RSACryptoService.cs @@ -17,26 +17,30 @@ public sealed class RSACryptoService : IRSACryptoService { private readonly ILogger _logger; private readonly ICertificateService _certificateService; - private readonly IOptions _options; + private readonly RSACryptoOptions _rsaOptions; private bool _disposed = false; private readonly X509Certificate2 _certificate; private readonly RSACryptoProvider _provider; - public RSACryptoService(ILogger logger, ICertificateService certificateService, IOptions options) + public RSACryptoService(ILogger logger, ICertificateService certificateService, IOptions rsaOptions) { this._logger = logger; this._certificateService = certificateService; - this._options = options; - - this._certificate = this._certificateService.GetConfiguredCertificate()!; - - var config = this._options.Value; - this._provider = new RSACryptoProvider(this._certificate, config.HashAlgorithmName, config.EncryptionPadding, config.SignaturePadding, config.Encoding); + this._rsaOptions = rsaOptions.Value; + + this._certificate = this._certificateService.GetCertificate("default", this._rsaOptions.Thumbprint, validOnly: true) + ?? throw new ArgumentNullException(); + + this._provider = new RSACryptoProvider( + this._certificate, + this._rsaOptions.HashAlgorithmName, + this._rsaOptions.EncryptionPadding, + this._rsaOptions.SignaturePadding, + this._rsaOptions.Encoding + ); } - #region Implementation - #region Properties /// @@ -136,6 +140,4 @@ public bool VerifyData(string data, string signature, Encoding? encoding = null) } #endregion - - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs b/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs index 1240a82..2774206 100644 --- a/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs +++ b/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs @@ -9,46 +9,53 @@ namespace AdvancedSystems.Security.Validators; -public sealed class CertificateOptionsValidator : IValidateOptions -{ - private readonly ILogger _logger; - private readonly ICertificateService _certificateService; - - public CertificateOptionsValidator(ILogger logger, ICertificateService certificateService) - { - this._logger = logger; - this._certificateService = certificateService; - } - - #region Implementation - - public ValidateOptionsResult Validate(string? name, CertificateOptions options) - { - this._logger.LogDebug("Started validation of {Options}", nameof(CertificateOptions)); - - if (string.IsNullOrEmpty(options.Thumbprint)) - { - return ValidateOptionsResult.Fail("Thumbprint is null or empty."); - } - - try - { - X509Certificate2? certificate = this._certificateService.GetConfiguredCertificate(validOnly: false); - - if (certificate is null) - { - return ValidateOptionsResult.Fail($"Configured certificate with thumbprint \"{options.Thumbprint}\" could not be found."); - } - } - catch (CertificateNotFoundException exception) - { - return ValidateOptionsResult.Fail(exception.Message); - } - - this._logger.LogDebug("Completed validation of {Options}", nameof(CertificateOptions)); - - return ValidateOptionsResult.Success; - } - - #endregion -} \ No newline at end of file +//public sealed class CertificateOptionsValidator : IValidateOptions +//{ +// private readonly ILogger _logger; +// private readonly ICertificateService _certificateService; + +// public CertificateOptionsValidator(ILogger logger, ICertificateService certificateService) +// { +// this._logger = logger; +// this._certificateService = certificateService; +// } + +// #region Implementation + +// public ValidateOptionsResult Validate(string? name, CertificateOptions options) +// { +// this._logger.LogDebug("Started validation of {Options}", nameof(CertificateOptions)); + +// if (string.IsNullOrEmpty(options.Thumbprint)) +// { +// return ValidateOptionsResult.Fail("Thumbprint is null or empty."); +// } + +// try +// { +// CertificateStoreOptions? store = options.Store; + +// if (store == null) +// { +// return ValidateOptionsResult.Fail("Certificate store is not configured."); +// } + +// X509Certificate2? certificate = this._certificateService.GetCertificate("default", options.Thumbprint, validOnly: false); + +// if (certificate is null) +// { +// return ValidateOptionsResult.Fail($"Configured certificate with thumbprint \"{options.Thumbprint}\" could not be found."); +// } +// } +// catch (CertificateNotFoundException exception) +// { +// return ValidateOptionsResult.Fail(exception.Message); +// } + +// this._logger.LogDebug("Completed validation of {Options}", nameof(CertificateOptions)); + +// return ValidateOptionsResult.Success; +// } + +// #endregion +//} \ No newline at end of file From 6f845c0bdabbdf5039b417f40a047ba1bfe082f6 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 12 Jan 2025 19:46:21 +0100 Subject: [PATCH 12/41] Fix parsing from appsettings.json --- .github/workflows/dotnet-tests.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index 4b0505b..b8918f4 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -38,24 +38,25 @@ jobs: if: runner.os == 'Windows' run: | $AppSettings = Get-Content '.\AdvancedSystems.Security.Tests\appsettings.json' -Raw | ConvertFrom-Json - $StoreSettings = $AppSettings.Certificate.Store - Import-Certificate -FilePath .\test.cer -CertStoreLocation "Cert:\$($StoreSettings.Location)\$($StoreSettings.Name)" + $Name = $AppSettings.CertificateStore.Name + $Location = $AppSettings.CertificateStore.Location + Import-Certificate -FilePath .\test.cer -CertStoreLocation "Cert:\$Location\$Name" shell: powershell - name: Import Test Certificate (Ubuntu) if: runner.os == 'Linux' run: | appSettings='./AdvancedSystems.Security.Tests/appsettings.json' - name=$(jq -r '.Certificate.Store.Name' $appSettings) - location=$(jq -r '.Certificate.Store.Location' $appSettings) + name=$(jq -r '.CertificateStore.Name' $appSettings) + location=$(jq -r '.CertificateStore.Location' $appSettings) dotnet certificate-tool add -f ./test.cer --store-name $name --store-location $location - name: Import Test Certificate (MacOS) if: runner.os == 'macOS' run: | appSettings='./AdvancedSystems.Security.Tests/appsettings.json' - name=$(jq -r '.Certificate.Store.Name' $appSettings) - location=$(jq -r '.Certificate.Store.Location' $appSettings) + name=$(jq -r '.CertificateStore.Name' $appSettings) + location=$(jq -r '.CertificateStore.Location' $appSettings) dotnet certificate-tool add -f ./test.cer --store-name $name --store-location $location - name: Test From 1158521e9938f79b55d85f679ab03d4865c05d53 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Mon, 13 Jan 2025 20:41:09 +0100 Subject: [PATCH 13/41] Add more tests to CertificateServiceTests --- .../Services/CertificateServiceTests.cs | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs index 62ea42d..47d34ae 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs @@ -1,4 +1,7 @@ -using System.Security.Cryptography.X509Certificates; +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Tests.Fixtures; @@ -23,9 +26,31 @@ public CertificateServiceTests(CertificateFixture certificateFixture) #region Tests + /// + /// Tests that + /// returns a collection of certificates from the configured certificate1 store. + /// + [Fact] + public void TestGetCertificate() + { + // Arrange + string storeService = this._sut.ConfiguredStoreService; + + // Act + ICertificateService? certificateService = this._sut.Host?.Services.GetService(); + int? certificateCount = certificateService?.GetCertificate(storeService).Count(); + + // Assert + Assert.Multiple(() => + { + Assert.NotNull(certificateCount); + Assert.InRange(certificateCount.Value, 1, int.MaxValue); + }); + } + /// /// Tests that - /// returns a certificate from the certificate store. + /// returns a certificate1 from the certificate1 store. /// [Fact] public void TestGetCertificate_ByThumbprint() @@ -46,5 +71,48 @@ public void TestGetCertificate_ByThumbprint() }); } + /// + /// Tests that + /// adds a self-signed test certificate to the certificate store, and that subsequently + /// removes the + /// self-signed test certificate from the certificate store. + /// + [Fact] + public void TestAddRemoveCertificate() + { + // Arrange + string storeService = this._sut.ConfiguredStoreService; + var certificate1 = CreateSelfSignedCertificate("O=AdvancedSystems"); + string thumbprint = certificate1.Thumbprint; + + // Act + var certificateService = this._sut.Host?.Services.GetService(); + bool isAdded = certificateService?.AddCertificate(storeService, certificate1) ?? false; + X509Certificate2? certificate2 = certificateService?.GetCertificate(storeService, thumbprint, validOnly: false); + bool isRemoved = certificateService?.RemoveCertificate(storeService, thumbprint) ?? false; + + // Assert + Assert.Multiple(() => + { + Assert.NotNull(certificateService); + Assert.True(isAdded); + Assert.NotNull(certificate2); + Assert.True(isRemoved); + }); + } + + #endregion + + #region Helpers + + private static X509Certificate2 CreateSelfSignedCertificate(string subject) + { + using var csdsa = ECDsa.Create(); + var request = new CertificateRequest(subject, csdsa, HashAlgorithmName.SHA256); + var validFrom = DateTimeOffset.UtcNow; + X509Certificate2 certificate = request.CreateSelfSigned(validFrom, validFrom.AddHours(1)); + return certificate; + } + #endregion } \ No newline at end of file From d06e2ca0a34ebb622f9a43fe17bc009533a5ef32 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 15 Jan 2025 21:22:13 +0100 Subject: [PATCH 14/41] Add doc strings to ICertificateService --- .../ICertificateService.cs | 146 +++++++++++++++++- .../ICertificateStore.cs | 1 + 2 files changed, 143 insertions(+), 4 deletions(-) diff --git a/AdvancedSystems.Security.Abstractions/ICertificateService.cs b/AdvancedSystems.Security.Abstractions/ICertificateService.cs index 811c703..f39af28 100644 --- a/AdvancedSystems.Security.Abstractions/ICertificateService.cs +++ b/AdvancedSystems.Security.Abstractions/ICertificateService.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; namespace AdvancedSystems.Security.Abstractions; @@ -8,24 +7,163 @@ namespace AdvancedSystems.Security.Abstractions; /// Defines a service for managing and retrieving X.509 certificates. /// /// +/// public interface ICertificateService { #region Methods + /// + /// Adds a certificate to a certificate store. + /// + /// + /// The name of the keyed service to use. + /// + /// + /// The certificate to add. + /// + /// + /// Returns if the was added + /// successfully to the certificate store, else . + /// bool AddCertificate(string storeService, X509Certificate2 certificate); + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// bool TryImportPemCertificate(string storeService, string publicKeyPath, string privateKeyPath, out X509Certificate2? certificate); + /// + /// Tries to import a PEM certificate file into a certificate store. + /// + /// + /// The name of the keyed service to use. + /// + /// + /// The file path to the PEM file containing the public key or certificate. + /// + /// + /// The file path to the PEM file containing the private key associated with the public key. + /// + /// + /// The password required to decrypt the private key in the PEM file. + /// + /// + /// An output parameter that will contain the imported instance if the operation succeeds; + /// otherwise, it will be . + /// + /// + /// if the certificate was imported to the certificate store successfully, else . + /// + /// + /// See also: . + /// bool TryImportPemCertificate(string storeService, string publicKeyPath, string privateKeyPath, string password, out X509Certificate2? certificate); - bool TryImportPfxCertificate(string storeService, string path, string password, out X509Certificate2? certificate); - + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// bool TryImportPfxCertificate(string storeService, string path, out X509Certificate2? certificate); + /// + /// Tries to import a PFX certificate file into a certificate store. + /// + /// + /// The name of the keyed service to use. + /// + /// + /// The file path to the PFX certificate file that needs to be imported. + /// + /// + /// The password required to access the PFX file's private key. + /// + /// + /// An output parameter that will contain the imported instance if the operation succeeds; + /// otherwise, it will be . + /// + /// + /// if the certificate was imported to the certificate store successfully, else . + /// + /// + /// See also: . + /// + bool TryImportPfxCertificate(string storeService, string path, string password, out X509Certificate2? certificate); + + /// + /// Retrieves all certificates from the certificate store. + /// + /// + /// The name of the keyed service to use. + /// + /// + /// Returns a collection of certificates. + /// IEnumerable GetCertificate(string storeService); + /// + /// Retrieves a certificate from the certificate store by using the . + /// + /// + /// The name of the keyed service to use. + /// + /// + /// The string representing the thumbprint of the certificate to retrieve. + /// + /// + /// to allow only valid certificates to be returned from the search; + /// otherwise, . + /// + /// + /// A object if a certificate in the certificate store + /// matches the search criteria, else . + /// X509Certificate2? GetCertificate(string storeService, string thumbprint, bool validOnly = true); + /// + /// Removes a certificate from the certificate store by using the . + /// + /// + /// The name of the keyed service to use. + /// + /// + /// The string representing the thumbprint of the certificate to remove. + /// + /// + /// Returns if a certificate with the specified + /// was removed from the certificate store, else . + /// bool RemoveCertificate(string storeService, string thumbprint); #endregion diff --git a/AdvancedSystems.Security.Abstractions/ICertificateStore.cs b/AdvancedSystems.Security.Abstractions/ICertificateStore.cs index 8200b8d..766d571 100644 --- a/AdvancedSystems.Security.Abstractions/ICertificateStore.cs +++ b/AdvancedSystems.Security.Abstractions/ICertificateStore.cs @@ -8,6 +8,7 @@ namespace AdvancedSystems.Security.Abstractions; /// /// Represents an X.509 store, which is a physical store where certificates are persisted and managed. /// +/// public interface ICertificateStore : IDisposable { #region Properties From 603978ba0f39a290f93302c30d21530c25b8b60b Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 15 Jan 2025 22:51:08 +0100 Subject: [PATCH 15/41] Configure CA and password certificate for testing purposes --- .github/workflows/dotnet-tests.yml | 23 +++++--- .../ServiceCollectionExtensionsTests.cs | 2 +- .../Services/CertificateServiceTests.cs | 2 +- .../Cryptography/DistinguishedName.cs | 2 +- development/AdvancedSystems-CA.pfx | Bin 0 -> 2758 bytes development/AdvancedSystems-Password.pem | 50 ++++++++++++++++++ development/about.md | 11 ++++ test.cer | Bin 786 -> 0 bytes 8 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 development/AdvancedSystems-CA.pfx create mode 100644 development/AdvancedSystems-Password.pem create mode 100644 development/about.md delete mode 100644 test.cer diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index b8918f4..f0481b3 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -31,33 +31,40 @@ jobs: - name: Restore Dotnet Tools run: dotnet tool restore - - name: Build + - name: Run Build run: dotnet build --no-restore - - name: Import Test Certificate (Windows) + - name: Import AdvancedSystems Certificate Authority + run: > + dotnet certificate-tool add --file ./development/AdvancedSystems-CA.pfx + --store-name My + --store-location CurrentUser + --password '${{ secrets.ADV_CA_SECRET }}' + + - name: Import Password Certificate (Windows) if: runner.os == 'Windows' run: | $AppSettings = Get-Content '.\AdvancedSystems.Security.Tests\appsettings.json' -Raw | ConvertFrom-Json $Name = $AppSettings.CertificateStore.Name $Location = $AppSettings.CertificateStore.Location - Import-Certificate -FilePath .\test.cer -CertStoreLocation "Cert:\$Location\$Name" + Import-Certificate -FilePath .\development\AdvancedSystems-Password.pem -CertStoreLocation "Cert:\$Location\$Name" shell: powershell - - name: Import Test Certificate (Ubuntu) + - name: Import Password Certificate (Ubuntu) if: runner.os == 'Linux' run: | appSettings='./AdvancedSystems.Security.Tests/appsettings.json' name=$(jq -r '.CertificateStore.Name' $appSettings) location=$(jq -r '.CertificateStore.Location' $appSettings) - dotnet certificate-tool add -f ./test.cer --store-name $name --store-location $location + dotnet certificate-tool add --file ./development/AdvancedSystems-Password.pem --store-name $name --store-location $location - - name: Import Test Certificate (MacOS) + - name: Import Password Certificate (MacOS) if: runner.os == 'macOS' run: | appSettings='./AdvancedSystems.Security.Tests/appsettings.json' name=$(jq -r '.CertificateStore.Name' $appSettings) location=$(jq -r '.CertificateStore.Location' $appSettings) - dotnet certificate-tool add -f ./test.cer --store-name $name --store-location $location + dotnet certificate-tool add --file ./development/AdvancedSystems-Password.pem --store-name $name --store-location $location - - name: Test + - name: Run Unit Tests run: dotnet test --no-build --verbosity normal diff --git a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index 4301c0f..d16071c 100644 --- a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -30,7 +30,7 @@ public async Task TestAddCertificateService_FromOptions() { // Arrange string storeService = "my/CurrentUser"; - string thumbprint = "A24421E3B4149A12B219AA67CD263D419829BD53"; + string thumbprint = "2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC"; using var hostBuilder = await new HostBuilder() .ConfigureWebHost(builder => builder diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs index 47d34ae..8a51cc8 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs @@ -57,7 +57,7 @@ public void TestGetCertificate_ByThumbprint() { // Arrange string storeService = this._sut.ConfiguredStoreService; - string thumbprint = "A24421E3B4149A12B219AA67CD263D419829BD53"; + string thumbprint = "2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC"; // Act var certificateService = this._sut.Host?.Services.GetService(); diff --git a/AdvancedSystems.Security/Cryptography/DistinguishedName.cs b/AdvancedSystems.Security/Cryptography/DistinguishedName.cs index 5d07ce1..84351d9 100644 --- a/AdvancedSystems.Security/Cryptography/DistinguishedName.cs +++ b/AdvancedSystems.Security/Cryptography/DistinguishedName.cs @@ -52,7 +52,7 @@ public sealed record DistinguishedName public string? OrganizationalUnit { get; init; } /// - /// Gets or sets the .. + /// Gets or sets the . /// /// /// diff --git a/development/AdvancedSystems-CA.pfx b/development/AdvancedSystems-CA.pfx new file mode 100644 index 0000000000000000000000000000000000000000..f7119ec3daad9785c9a0a9d4cccf7fecbae490a7 GIT binary patch literal 2758 zcmZXUc{CJ`9>vEr%wl~ep&w&OV;_U;Ta0ZK5>X<;ObOXCg$mPHM}{Fw)`TJ?OQ>YY zWEY{x9%k%A8WNf3o%7!Ny>s3l-*fM|_j~T2A2box2IM$`Cc+ZApb9aTF+02*V2%PJ z>>h{+3q6cO&_vMA-xXH@5wv(1%>y|Ahcfwh!+{p#g8h4e=Wvk^1lftA_i-~sQ9vLB z5J?18H8PU@fi9&Xg@nQ@CrE3KeVhG|z2euYroOpWa~{yJ=ud?pSO~Ie-ddmei!uEa ztD`MnL71e1YE4jrb-Qv0m0A+B*Ky4FaGO%Tb4wl_?GBC_ zmDhJ!){@m$B>$H0AHq}(MJJBBk>5R2eDoYp*>!?{r(AqEIz4ONEr$Y_kp^3v*}Xfx zE#zP$cpUNvAXu2swrG!d9>b%d`MEADY4e~@cfW$Pq9an<0C8F_;KHoDGA*xU&Egc; zWMtfl(AczQW+TDnsJfFG_Hw|qw|Wb2y{xuLZzMTtVn?#U zS>`0M=>xxeDt|OMDqqm}{0hj=%+}dutr4obNBSkA-(QgSC{gV*?KIh&u!%VdJ@=-* zN^;BKp%oJ0AlaFE^M_|f#7j=Al=+1W7i2&+{)(7rNOZraLM9Y*5V)ut)wyV+;3Jg8_T|bR8h^=8)e3u z)kQ>S&*PZ(?xQ+|fpF1vf0e$M9;Z@3^1j)QW?^p*nxGE-2rZ;OIWB%uXA(mSOep$o zZf@ltlpr&AefSayz9j1^YrD)Kb>3a846~xL>b9}lG909{Q+6ASH~ucmX{NVb3h$Xd zr;Ov|G?L-Hvmfr)CLsU?tL)vxaJMCcfLTkJ;_&V+Y4tqzD~pPpJl#T!7ww?dF6oLU zsj(z7YEqD1{wrZ^mKRCGG!Od`XP(rflOFSCUd@qO3S$kqZ!(K5kl?A9B(h%Y@Bux zhaw%ihfDPeW$mwHg|7C=32y1VX7EdMN}Z0!Pp$tz$BC%#LxRtF1M(5?6Om#|eeSRI_V5%)jQQ5yygfxU)TNw? z5&Vw_w|NC$^l(0oblP9HOn*jZOTfV2-SPba;kqru8`lK74AxTxd=t@ee#h6}-pKEk z{n1v$9a(`UauWUra0NupYak-0=V9!2c$c}j|ARXi2k?-K>xV#G{(lOVAZx*U@)6;nThJfUiUy^;R_Zi&Xj~fgWIN3!(_`|+g=$0g7Ci3qp_7hYhmx>0+7 zS)LE=EvPQmuNLW+J9f$!>xGxJB7~gey{Hb1kkR6Okm_L+^k5>JAfBB}mQIb*_!N3} zn%{!mg$y`F*Sae(m_bQ?&&nLx``u3W##gO zIA-c~SmOR{`knOJD%v3YC=E+}V4*O$nMF~{eEa*t7Q~miP#Z+;G@^%8UAA@6KkZI; zAvQRqPy52)Q4QwWtKmBq@Q=l5at28=5sRD@vyCQXghAMeJ6K zT8&H-04d2}RG#OC!^}5zZiw`VNToGWE!Y~SxX zp~z8-kBN|R)q>Lfu`QPkm7$6a%~82)PG$Lz^@bIB=4B_y7VKSD^MU=Z1PfLFaUY|> zUd4Kbkv};?11DeZG#kF@(@rHYxRTr6m6b&&LAu&QpLTy4(5k$Ccjcw9qv2P3RO*F; zQNI#9gNo4#kAU&rMaYh4Q`-0nI1kQuz)_cK)MZGPBzFv%yXOofXHfRM4qlVKIYz^T z63$*ohCDo}Z_}#tGG_DRqb$g}($fYF&z1+z{`9nOtIXO?IC<%9^N+oE=}9h;z$hz4 zj4qfCSb@t=imcj32jg zrVknpZUUl>-aA`647}fnIOA=7bI&_NFRGj2+3VSg(3xuOuCwH`?UBHkXuMh)tGG1p z_|V`ac8%ro>8^zL_-0KM6)E8!_?ITbvT8C{P`;9E#jhBusgJ+9-3$rQ(2tS1hYy={*fvL72Mpi7l?`s!4lS9oVqT)yb- zbT)KA^Mdn3l1$bIwHjm4JSxk$KBG)Id;zx4+QWOR1AgPd+yU1nuC?*6eq9#HCM*@s z#H5S8pn6%03coZqY}WH1(9xda8nLI0JCYiM>_A`9-59C$$DI+*vxwnS0xZ5 zVC(;C?k4)#k=FP;v=dFZ$Q6^D@S^`}5mq+dSWnd%ed51#gbJx1107Y|7ZWKzo;5FL z4c`2`p{kVhLVX{t{`b>SRU|?NU<~jA5CB&JHvygi+@ZGzgaU#Aw+_AEp(+C~0AsW= xS_%#2;#A~20zA$M03E#wh&j5587gG}>Bg5gYz#a}J`AoZqD-NXI=MXsLH#fiS=h|b_&Yux= z<2$9H7Ss^V=M16?|W#XVz6h~?=u2VT=#SD zHdy31C9!6C$@3|Th0ojlv9Z*cV4Or{@N-%b%k2l z7lpa|LX3VdZ#-rox#F9ngw4%adzwNoU2pMLk?H%(#LURRxH!PT&wvjY`m+3tjQ?3! zn3-4?7|4S7sw`p#B5WMmY>cd|?95DX79&KOk420{#Po`hyk5rQ#3G~kdFC_jv{=TQ zdLxH9FnWPu&d9JX|74_8Qo=`}?V=pLY;q?Ir(Tbr=e{nf+gB*~L#pF9YlhsJI)`=N zY>NJJe{uJ{-t@O+A3t5WdF;)Ojg8X&^)nxy?aR6$Sf9^SlJHR>z2eI<{_T6enJ`#+=LrrfS?9*bd2F+v0R?%Qeeir%#k6Xl`0~t!t-X ZjqA~dectO_CZ}BUHu{?V>p;u2MgYq^IR*d# From 042d83d8035d85835ed4634b0466450a539e1728 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 15 Jan 2025 23:29:19 +0100 Subject: [PATCH 16/41] Run test in configuration mode Release --- .github/workflows/dotnet-tests.yml | 11 +++++++++-- .husky/task-runner.json | 11 +++++++++-- development/about.md | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index f0481b3..401050d 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -32,7 +32,11 @@ jobs: run: dotnet tool restore - name: Run Build - run: dotnet build --no-restore + run: > + dotnet build ./AdvancedSystems.Security + --configuration Release + --no-restore + /warnAsError - name: Import AdvancedSystems Certificate Authority run: > @@ -67,4 +71,7 @@ jobs: dotnet certificate-tool add --file ./development/AdvancedSystems-Password.pem --store-name $name --store-location $location - name: Run Unit Tests - run: dotnet test --no-build --verbosity normal + run: > + dotnet test ./AdvancedSystems.Security.Tests + --configuration Release + --verbosity normal diff --git a/.husky/task-runner.json b/.husky/task-runner.json index 57978a0..cb037bf 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -23,7 +23,10 @@ "command": "dotnet", "args": [ "build", - "/warnaserror" + "./AdvancedSystems.Security", + "--configuration", + "Release", + "/warnAsError" ] }, { @@ -32,7 +35,11 @@ "command": "dotnet", "args": [ "test", - "--nologo" + "./AdvancedSystems.Security.Tests", + "--configuration", + "Release", + "--verbosity", + "minimal" ] } ] diff --git a/development/about.md b/development/about.md index 2b568a2..8b196b4 100644 --- a/development/about.md +++ b/development/about.md @@ -1,6 +1,6 @@ # About -This folder contains self-signed test certificates that were created for testing +This folder contains self-signed test certificates that were created for development purposes only. ## Certificates From fdda6b4d1b6c51148f33360b1e0b7037812d0669 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 15 Jan 2025 23:57:11 +0100 Subject: [PATCH 17/41] Format workflow files --- .../workflows/dotnet-publish-abstractions.yml | 30 +++++++++++++++---- .github/workflows/dotnet-publish-core.yml | 30 +++++++++++++++---- .github/workflows/dotnet-tests.yml | 2 +- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/.github/workflows/dotnet-publish-abstractions.yml b/.github/workflows/dotnet-publish-abstractions.yml index 87149a0..9db2996 100644 --- a/.github/workflows/dotnet-publish-abstractions.yml +++ b/.github/workflows/dotnet-publish-abstractions.yml @@ -38,16 +38,36 @@ jobs: shell: powershell - name: Restore Dependencies - run: dotnet restore ${{ env.PROJECT_PATH }} --configfile nuget.config + run: > + dotnet restore ${{ env.PROJECT_PATH }} + --configfile nuget.config - name: Build Project - run: dotnet build ${{ env.PROJECT_PATH }} --nologo --no-restore --configuration Release + run: > + dotnet build ${{ env.PROJECT_PATH }} + --configuration Release + --no-restore + --nologo - name: Pack Project - run: dotnet pack ${{ env.PROJECT_PATH }} --nologo --no-restore --no-build --configuration Release --output ${{ env.RELEASE_DIRECTORY }} + run: > + dotnet pack ${{ env.PROJECT_PATH }} + --configuration Release + --output ${{ env.RELEASE_DIRECTORY }} + --no-restore + --no-build + --nologo - name: Deploy Package to GitHub - run: dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg' --skip-duplicate --api-key ${{ secrets.GH_AUTH_TOKEN_PUSH }} --source ${{ env.GITHUB_SOURCE }} + run: > + dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg' + --api-key ${{ secrets.GH_AUTH_TOKEN_PUSH }} + --source ${{ env.GITHUB_SOURCE }} + --skip-duplicate - name: Deploy Package to NuGet - run: dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg' --skip-duplicate --api-key ${{ secrets.NUGET_AUTH_TOKEN_PUSH }} --source ${{ env.NUGET_SOURCE }} + run: > + dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg' + --api-key ${{ secrets.NUGET_AUTH_TOKEN_PUSH }} + --source ${{ env.NUGET_SOURCE }} + --skip-duplicate diff --git a/.github/workflows/dotnet-publish-core.yml b/.github/workflows/dotnet-publish-core.yml index adb9da2..7f4465b 100644 --- a/.github/workflows/dotnet-publish-core.yml +++ b/.github/workflows/dotnet-publish-core.yml @@ -38,16 +38,36 @@ jobs: shell: powershell - name: Restore Dependencies - run: dotnet restore ${{ env.PROJECT_PATH }} --configfile nuget.config + run: > + dotnet restore ${{ env.PROJECT_PATH }} + --configfile nuget.config - name: Build Project - run: dotnet build ${{ env.PROJECT_PATH }} --nologo --no-restore --configuration Release + run: > + dotnet build ${{ env.PROJECT_PATH }} + --configuration Release + --no-restore + --nologo - name: Pack Project - run: dotnet pack ${{ env.PROJECT_PATH }} --nologo --no-restore --no-build --configuration Release --output ${{ env.RELEASE_DIRECTORY }} + run: > + dotnet pack ${{ env.PROJECT_PATH }} + --configuration Release + --output ${{ env.RELEASE_DIRECTORY }} + --no-restore + --no-build + --nologo - name: Deploy Package to GitHub - run: dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg' --skip-duplicate --api-key ${{ secrets.GH_AUTH_TOKEN_PUSH }} --source ${{ env.GITHUB_SOURCE }} + run: > + dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg' + --api-key ${{ secrets.GH_AUTH_TOKEN_PUSH }} + --source ${{ env.GITHUB_SOURCE }} + --skip-duplicate - name: Deploy Package to NuGet - run: dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg' --skip-duplicate --api-key ${{ secrets.NUGET_AUTH_TOKEN_PUSH }} --source ${{ env.NUGET_SOURCE }} + run: > + dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg' + --api-key ${{ secrets.NUGET_AUTH_TOKEN_PUSH }} + --source ${{ env.NUGET_SOURCE }} + --skip-duplicate diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index 401050d..42ed84b 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -31,7 +31,7 @@ jobs: - name: Restore Dotnet Tools run: dotnet tool restore - - name: Run Build + - name: Build Project run: > dotnet build ./AdvancedSystems.Security --configuration Release From 77f4b31dc60c12ff776da636b0683fd2c59f115a Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Thu, 16 Jan 2025 19:50:01 +0100 Subject: [PATCH 18/41] Add unit test to CertificateExtensions --- .../Extensions/CertificateExtensionsTests.cs | 51 ++++++++++++++++++- .../Extensions/CertificateExtensions.cs | 3 +- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs index 65eacdb..09f42e6 100644 --- a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs @@ -1,6 +1,13 @@ -using AdvancedSystems.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Extensions; +using AdvancedSystems.Security.Tests.Fixtures; + +using Microsoft.Extensions.DependencyInjection; + using Xunit; namespace AdvancedSystems.Security.Tests.Extensions; @@ -8,10 +15,50 @@ namespace AdvancedSystems.Security.Tests.Extensions; /// /// Tests the public methods in . /// -public sealed class CertificateExtensionsTests +public sealed class CertificateExtensionsTests : IClassFixture { + private readonly CertificateFixture _certificateFixture; + + public CertificateExtensionsTests(CertificateFixture certificateFixture) + { + this._certificateFixture = certificateFixture; + } + #region Tests + /// + /// Tests that + /// parses the DN from a certificate retrieved from a certificate store correctly. + /// + [Fact] + public void TestTryParseDistinguishedName_FromCertificate() + { + // Arrange + string storeService = this._certificateFixture.ConfiguredStoreService; + string thumbprint = "2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC"; + + // Act + ICertificateService? certificateService = this._certificateFixture.Host?.Services.GetService(); + X509Certificate2? certificate = certificateService?.GetCertificate(storeService, thumbprint, validOnly: false); + bool isDsn = CertificateExtensions.TryParseDistinguishedName(certificate?.Subject ?? string.Empty, out DistinguishedName? dn); + + // Assert + Assert.Multiple(() => + { + Assert.NotNull(certificateService); + Assert.NotNull(certificate); + Assert.NotNull(certificate?.Subject); + Assert.True(isDsn); + Assert.NotNull(dn); + Assert.Equal("DE", dn?.Country); + Assert.Equal("Berlin", dn?.State); + Assert.Equal("Berlin", dn?.Locality); + Assert.Equal("Advanced Systems", dn?.Organization); + Assert.Equal("RnD", dn?.OrganizationalUnit); + Assert.Equal("AdvancedSystems-CA", dn?.CommonName); + }); + } + /// /// Tests that /// extracts the RDNs from the DN correctly from the string. diff --git a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs index ae9888a..c0dc448 100644 --- a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs +++ b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Security.Cryptography; @@ -102,7 +103,7 @@ public static partial class CertificateExtensions /// /// /// - public static bool TryParseDistinguishedName(string distinguishedName, out DistinguishedName? result) + public static bool TryParseDistinguishedName(string distinguishedName, [NotNullWhen(true)] out DistinguishedName? result) { var rdns = new Dictionary(StringComparer.OrdinalIgnoreCase); var dn = new X500DistinguishedName(distinguishedName); From 12d5f791ce95b81e8e4a9813d25095cd691a091f Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Thu, 16 Jan 2025 20:18:45 +0100 Subject: [PATCH 19/41] Test TryImportPfxCertificate --- .github/workflows/dotnet-tests.yml | 4 +++ .../AdvancedSystems.Security.Tests.csproj | 16 +++++----- .../Helpers/UserSecrets.cs | 9 ++++++ .../Services/CertificateServiceTests.cs | 29 +++++++++++++++++++ readme.md | 7 +++++ 5 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index 42ed84b..ef8963e 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -70,6 +70,10 @@ jobs: location=$(jq -r '.CertificateStore.Location' $appSettings) dotnet certificate-tool add --file ./development/AdvancedSystems-Password.pem --store-name $name --store-location $location + - name: Configure DotNet User Secrets + run: | + dotnet user-secrets set CertificatePassword '${{ secrets.ADV_CA_SECRET }}' --project ./AdvancedSystems.Security.Tests + - name: Run Unit Tests run: > dotnet test ./AdvancedSystems.Security.Tests diff --git a/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj b/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj index e4a25e3..c652eeb 100644 --- a/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj +++ b/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj @@ -6,29 +6,31 @@ AdvancedSystems.Security.Tests AdvancedSystems.Security.Tests true + d53eb55d-dc13-4abe-b60b-e11e90d1afbd - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs b/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs new file mode 100644 index 0000000..6c7605f --- /dev/null +++ b/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs @@ -0,0 +1,9 @@ +namespace AdvancedSystems.Security.Tests.Helpers; + +internal static class UserSecrets +{ + /// + /// Retrieves the certificate password from the secrets store. + /// + public const string CERTIFICATE_PASSWORD = "CertificatePassword"; +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs index 8a51cc8..57f83fa 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs @@ -1,11 +1,14 @@ using System; +using System.IO; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Tests.Fixtures; +using AdvancedSystems.Security.Tests.Helpers; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -26,6 +29,32 @@ public CertificateServiceTests(CertificateFixture certificateFixture) #region Tests + [SkippableFact] + public void TestTryImportPfxCertificate() + { + // Arrange + string storeService = this._sut.ConfiguredStoreService; + string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "development", "AdvancedSystems-CA.pfx"); + + var configuration = new ConfigurationBuilder() + .AddUserSecrets() + .Build(); + + string? password = configuration[UserSecrets.CERTIFICATE_PASSWORD]; + Skip.If(string.IsNullOrEmpty(password)); + + // Act + ICertificateService? certificateService = this._sut.Host?.Services.GetService(); + bool? isImported = certificateService?.TryImportPfxCertificate(storeService, path, password, out _); + + // Assert + Assert.Multiple(() => + { + Assert.NotNull(certificateService); + Assert.True(isImported.HasValue); + }); + } + /// /// Tests that /// returns a collection of certificates from the configured certificate1 store. diff --git a/readme.md b/readme.md index a7c00af..5e1c648 100644 --- a/readme.md +++ b/readme.md @@ -31,6 +31,13 @@ for debugging .NET assemblies. ## Development Environment +Configure local user secrets for the test suite (optional): + +```powershell +$Password = Read-Host -Prompt "AdvancedSystems-CA.pfx Password" +dotnet user-secrets set CertificatePassword $Password --project ./AdvancedSystems.Tests +``` + Run test suite: ```powershell From 89b605933358b329584e044d5f741daa0d239cfc Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Thu, 16 Jan 2025 21:14:42 +0100 Subject: [PATCH 20/41] Add placeholder test functions --- .../Services/CertificateServiceTests.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs index 57f83fa..001e917 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs @@ -29,6 +29,28 @@ public CertificateServiceTests(CertificateFixture certificateFixture) #region Tests + [Fact(Skip = "TODO")] + public void TestTryImportPemCertificate_PKCS8_Header() + { + + } + + [Fact(Skip = "TODO")] + public void TestTryImportPemCertificate_Encrypted_PKCS8_Header() + { + + } + + [Fact(Skip = "TODO")] + public void TestTryImportPemCertificate_RSA_Header() + { + + } + + /// + /// Tests that + /// successfully imports a password-protected PFX certificate. + /// [SkippableFact] public void TestTryImportPfxCertificate() { @@ -41,7 +63,7 @@ public void TestTryImportPfxCertificate() .Build(); string? password = configuration[UserSecrets.CERTIFICATE_PASSWORD]; - Skip.If(string.IsNullOrEmpty(password)); + Skip.If(string.IsNullOrEmpty(password), "A dotnet user-secrets is not configured for this test."); // Act ICertificateService? certificateService = this._sut.Host?.Services.GetService(); From fe70b277a475a05ccb3474ac032bdda77337d060 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 17 Jan 2025 21:00:27 +0100 Subject: [PATCH 21/41] Improve doc strings --- .../ICertificateStore.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/AdvancedSystems.Security.Abstractions/ICertificateStore.cs b/AdvancedSystems.Security.Abstractions/ICertificateStore.cs index 766d571..d9b3b72 100644 --- a/AdvancedSystems.Security.Abstractions/ICertificateStore.cs +++ b/AdvancedSystems.Security.Abstractions/ICertificateStore.cs @@ -2,6 +2,7 @@ using System.Security; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using System.Security.Permissions; namespace AdvancedSystems.Security.Abstractions; @@ -82,7 +83,7 @@ public interface ICertificateStore : IDisposable /// The certificate to add. /// /// - /// Thrown when is null. + /// Thrown when is . /// /// /// Thrown when the certificate could not be added to the store. @@ -96,7 +97,7 @@ public interface ICertificateStore : IDisposable /// The collection of certificates to add. /// /// - /// Thrown when is null. + /// Thrown when is . /// /// /// Thrown when the caller does not have the required permission. @@ -114,7 +115,7 @@ public interface ICertificateStore : IDisposable /// The certificate to remove. /// /// - /// Thrown when is null. + /// Thrown when is . /// /// /// Thrown when the caller does not have the required permission. @@ -128,7 +129,7 @@ public interface ICertificateStore : IDisposable /// A range of certificates to remove. /// /// - /// Thrown when is null. + /// Thrown when is . /// /// /// Thrown when the caller does not have the required permission. From 70ab33526f7ad7f402251a0155c09b81b3512ba1 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 17 Jan 2025 21:02:05 +0100 Subject: [PATCH 22/41] Refactor and re-organize extension methods --- .../Cryptography/HashTests.cs | 1 + AdvancedSystems.Security/Common/Bytes.cs | 90 ------------------- AdvancedSystems.Security/Extensions/Bytes.cs | 23 +++++ .../Extensions/SecureStringExtensions.cs | 88 ++++++++++++++++++ .../Services/HashService.cs | 1 + 5 files changed, 113 insertions(+), 90 deletions(-) delete mode 100644 AdvancedSystems.Security/Common/Bytes.cs create mode 100644 AdvancedSystems.Security/Extensions/Bytes.cs create mode 100644 AdvancedSystems.Security/Extensions/SecureStringExtensions.cs diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs index 1fc11c6..6c5bc07 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs @@ -3,6 +3,7 @@ using AdvancedSystems.Security.Common; using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; using Xunit; diff --git a/AdvancedSystems.Security/Common/Bytes.cs b/AdvancedSystems.Security/Common/Bytes.cs deleted file mode 100644 index c38e6d1..0000000 --- a/AdvancedSystems.Security/Common/Bytes.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Security; -using System.Text; - -namespace AdvancedSystems.Security.Common; - -/// -/// Implements various utility methods for manipulating byte arrays and spans in cryptographic operations. -/// -public static class Bytes -{ - public static string ToString(this byte[] array, Format format) - { - return format switch - { - Format.Hex => BitConverter.ToString(array).Replace("-", string.Empty).ToLower(), - Format.Base64 => Convert.ToBase64String(array), - Format.String => Encoding.UTF8.GetString(array), - _ => throw new NotSupportedException($"String formatting is not implemted for {format}.") - }; - } - - public static byte[] GetBytes(T value) where T : INumber => value switch - { - bool boolValue => BitConverter.GetBytes(boolValue), - char charValue => BitConverter.GetBytes(charValue), - double doubleValue => BitConverter.GetBytes(doubleValue), - Half halfValue => BitConverter.GetBytes(halfValue), - short shortValue => BitConverter.GetBytes(shortValue), - int intValue => BitConverter.GetBytes(intValue), - long longValue => BitConverter.GetBytes(longValue), - float floatValue => BitConverter.GetBytes(floatValue), - ushort ushortValue => BitConverter.GetBytes(ushortValue), - uint uintValue => BitConverter.GetBytes(uintValue), - ulong ulongValue => BitConverter.GetBytes(ulongValue), - _ => throw new ArgumentException("Failed to convert T to an array of bytes.", nameof(value)) - }; - - /// - /// Gets the bytes of a object. - /// - /// - /// Represents a text that should be kept confidential, such as a password. - /// - /// - /// The character encoding used to convert all characters in the underlying - /// into a sequence of bytes. - /// - /// - /// Returns the bytes of the current instance. - /// - /// - /// This method exposes the internal state of through marshaling. - /// It is no longer recommended to use the for new development - /// on .NET Core projects. - /// - /// - /// Thrown when is null. - /// - /// - [Obsolete] - public static byte[] GetBytes(this SecureString secureString, Encoding? encoding = null) - { - ArgumentNullException.ThrowIfNull(secureString, nameof(secureString)); - encoding ??= Encoding.UTF8; - IntPtr strPtr = IntPtr.Zero; - - try - { - strPtr = Marshal.SecureStringToGlobalAllocUnicode(secureString); - return encoding.GetBytes(Marshal.PtrToStringUni(strPtr)!); - } - finally - { - if (strPtr != IntPtr.Zero) - { - Marshal.ZeroFreeGlobalAllocUnicode(strPtr); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Equals(this byte[] lhs, byte[] rhs) - { - return new Span(lhs).SequenceEqual(rhs); - } -} \ No newline at end of file diff --git a/AdvancedSystems.Security/Extensions/Bytes.cs b/AdvancedSystems.Security/Extensions/Bytes.cs new file mode 100644 index 0000000..bd4ed7c --- /dev/null +++ b/AdvancedSystems.Security/Extensions/Bytes.cs @@ -0,0 +1,23 @@ +using System; +using System.Text; + +using AdvancedSystems.Security.Common; + +namespace AdvancedSystems.Security.Extensions; + +/// +/// Implements extension methods for manipulating byte arrays. +/// +public static class Bytes +{ + public static string ToString(this byte[] array, Format format) + { + return format switch + { + Format.Hex => BitConverter.ToString(array).Replace("-", string.Empty).ToLower(), + Format.Base64 => Convert.ToBase64String(array), + Format.String => Encoding.UTF8.GetString(array), + _ => throw new NotSupportedException($"String formatting is not implemted for {format}.") + }; + } +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Extensions/SecureStringExtensions.cs b/AdvancedSystems.Security/Extensions/SecureStringExtensions.cs new file mode 100644 index 0000000..59839e4 --- /dev/null +++ b/AdvancedSystems.Security/Extensions/SecureStringExtensions.cs @@ -0,0 +1,88 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; + +namespace AdvancedSystems.Security.Extensions; + +public static class SecureStringExtensions +{ + /// + /// Gets the bytes of a object. + /// + /// + /// Represents a text that should be kept confidential, such as a password. + /// + /// + /// The character encoding used to convert all characters in the underlying + /// into a sequence of bytes. + /// + /// + /// Returns the bytes of the current instance. + /// + /// + /// + /// This method exposes the internal state of through marshaling. + /// It is no longer recommended to use the for new development + /// on .NET CryptoCore projects. + /// + /// + /// See also: . + /// + /// + /// + /// Thrown when is . + /// + [Obsolete] + public static byte[] GetBytes(this SecureString secureString, Encoding? encoding = null) + { + ArgumentNullException.ThrowIfNull(secureString, nameof(secureString)); + encoding ??= Encoding.UTF8; + IntPtr strPtr = IntPtr.Zero; + + try + { + strPtr = Marshal.SecureStringToGlobalAllocUnicode(secureString); + return encoding.GetBytes(Marshal.PtrToStringUni(strPtr)!); + } + finally + { + if (strPtr != IntPtr.Zero) + { + Marshal.ZeroFreeGlobalAllocUnicode(strPtr); + } + } + } + + /// + /// Returns an managed derived from the contents of a managed object. + /// + /// + /// Represents a text that should be kept confidential, such as a password. + /// + /// + /// A managed string that holds the contents of the object. + /// + /// + /// + /// + [Obsolete] + public static string ToString(this SecureString secureString) + { + ArgumentNullException.ThrowIfNull(secureString, nameof(secureString)); + IntPtr strPtr = IntPtr.Zero; + + try + { + strPtr = Marshal.SecureStringToBSTR(secureString); + return Marshal.PtrToStringBSTR(strPtr); + } + finally + { + if (strPtr != IntPtr.Zero) + { + Marshal.ZeroFreeBSTR(strPtr); + } + } + } +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Services/HashService.cs b/AdvancedSystems.Security/Services/HashService.cs index 7e4288f..bd3186c 100644 --- a/AdvancedSystems.Security/Services/HashService.cs +++ b/AdvancedSystems.Security/Services/HashService.cs @@ -3,6 +3,7 @@ using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Common; using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; using Microsoft.Extensions.Logging; From bf2406fc7d3e6c4be05a212d605cdfc044bb963e Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 17 Jan 2025 21:03:47 +0100 Subject: [PATCH 23/41] More doc improvements --- AdvancedSystems.Security/Internals/Hazmat.cs | 10 +++++----- readme.md | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/AdvancedSystems.Security/Internals/Hazmat.cs b/AdvancedSystems.Security/Internals/Hazmat.cs index 41b315a..ea37cdb 100644 --- a/AdvancedSystems.Security/Internals/Hazmat.cs +++ b/AdvancedSystems.Security/Internals/Hazmat.cs @@ -3,11 +3,11 @@ namespace AdvancedSystems.Security.Internals; /// -/// Defines low-level cryptographic primitives that can be dangerous if used incorrectly. -/// Because of the high risk accociated with working at this level, this is referred to -/// as the "hazardous materials" or "hazmat" layer. +/// Functions defined in this class can have dangerous security implications if +/// used incorrectly. Because of the high risk accociated with working at this level, +/// this is referred to as the "hazardous materials" (short: "hazmat") layer. /// -internal static class Hazmat +public static class Hazmat { /// /// Gets an array of type and size @@ -25,7 +25,7 @@ internal static class Hazmat /// Skipping zero-initialization using this API only has a material performance benefit for /// large arrays, such as buffers of several kilobytes or more. /// - internal static T[] GetUninitializedArray(int size) where T : unmanaged + public static T[] GetUninitializedArray(int size) where T : unmanaged { // If pinned is set to true, T must not be a reference type or a type that contains object references. return GC.AllocateUninitializedArray(size, pinned: true); diff --git a/readme.md b/readme.md index 5e1c648..764c837 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ dotnet user-secrets set CertificatePassword $Password --project ./AdvancedSystem Run test suite: ```powershell -dotnet test .\AdvancedSystems.Core.Tests\ --no-logo +dotnet test ./AdvancedSystems.Core.Tests --configuration Release ``` In addition to unit testing, this project also uses stryker for mutation testing, which is setup to be installed with @@ -59,5 +59,5 @@ dotnet stryker Build and serve documentation locally (`http://localhost:8080`): ```powershell -docfx .\docs\docfx.json --serve +docfx ./docs/docfx.json --serve ``` From 659ec79881dc48bf918077af74892a9b21c7f928 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 17 Jan 2025 21:05:48 +0100 Subject: [PATCH 24/41] Implement TryComputeSecure for password-based key derivation --- AdvancedSystems.Security/Cryptography/Hash.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/AdvancedSystems.Security/Cryptography/Hash.cs b/AdvancedSystems.Security/Cryptography/Hash.cs index 3af5f0e..394e34e 100644 --- a/AdvancedSystems.Security/Cryptography/Hash.cs +++ b/AdvancedSystems.Security/Cryptography/Hash.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Security.Cryptography; @@ -43,9 +44,83 @@ public static HashAlgorithm Create(HashAlgorithmName hashAlgorithmName) /// /// The computed hash code. /// + /// + /// WARNING: Do not use this method to compute hashes for confidential data (e.g., passwords). + /// Instead, use + /// + /// for secure hashing. + /// public static byte[] Compute(byte[] buffer, HashAlgorithmName hashAlgorithmName) { using var hashAlgorithm = Hash.Create(hashAlgorithmName); + return hashAlgorithm.ComputeHash(buffer); } + + /// + /// Creates a cryptographically secure hash value for the specified byte array as a PBKDF2 derived key. + /// + /// + /// The password used to derive the hash. + /// + /// + /// The salt used to derive the hash. + /// + /// + /// The size of hash to derive. + /// + /// + /// The number of iterations for the operation. + /// + /// + /// The hash algorithm to use to derive the hash. Supported algorithms are: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// A byte array containing the created PBKDF2 derived hash. + /// + /// + /// if the operation succeeds; otherwise, . + /// + /// + /// Notes on usage: + /// + /// + /// The has to be stored alongside the password hash and + /// count. + /// + /// + /// A higher count results in more computational + /// overhead, thus slowing down this function invocation considerably. This behavior + /// is intentional to mitigate brute-force attacks on leaked databases. + /// + /// + /// See also: . + /// + public static bool TryComputeSecure(byte[] password, byte[] salt, int hashSize, int iterations, HashAlgorithmName hashAlgorithmName, [NotNullWhen(true)] out byte[] pbkdf2) + { + try + { + pbkdf2 = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, hashAlgorithmName, hashSize); + return true; + } + catch (Exception) + { + pbkdf2 = []; + return false; + } + } } \ No newline at end of file From b2a57411defe22bea3b473969797d35a24beb9ff Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 17 Jan 2025 21:51:51 +0100 Subject: [PATCH 25/41] Implement GetSecureHash methods in IHashService --- .../IHashService.cs | 177 ++++++++++++++++-- AdvancedSystems.Security/Cryptography/Hash.cs | 23 +-- .../Services/HashService.cs | 36 ++++ 3 files changed, 205 insertions(+), 31 deletions(-) diff --git a/AdvancedSystems.Security.Abstractions/IHashService.cs b/AdvancedSystems.Security.Abstractions/IHashService.cs index 45ae618..c302956 100644 --- a/AdvancedSystems.Security.Abstractions/IHashService.cs +++ b/AdvancedSystems.Security.Abstractions/IHashService.cs @@ -10,66 +10,215 @@ public interface IHashService #region Methods /// - /// Computes the MD5 hash value for the specified byte array. + /// Computes the MD5 hash value for the specified . /// /// /// The input to compute the hash code for. /// /// - /// The hexadecimal representation of the computed hash code. + /// The hexadecimal representation of the computed hash code. /// - /// + /// + /// + /// See also: + /// + /// [Obsolete("MD5 is not a cryptographically secure hash algorithm.")] string GetMD5Hash(byte[] buffer); /// - /// Computes the SHA1 hash value for the specified byte array. + /// Computes the SHA1 hash value for the specified . /// /// /// The input to compute the hash code for. /// /// - /// The hexadecimal representation of the computed hash code. + /// The hexadecimal representation of the computed hash code. /// + /// + /// + /// WARNING: Do not use this method to compute hashes for confidential data + /// (e.g., passwords). Instead, use + /// + /// for secure hashing instead. + /// + /// + /// See also: + /// + /// /// [Obsolete("SHA1 is not a cryptographically secure hash algorithm.")] string GetSHA1Hash(byte[] buffer); /// - /// Computes the SHA256 hash value for the specified byte array. + /// Computes the SHA256 hash value for the specified . /// /// /// The input to compute the hash code for. /// /// - /// The hexadecimal representation of the computed hash code. + /// The hexadecimal representation of the computed hash code. /// - /// + /// + /// + /// WARNING: Do not use this method to compute hashes for confidential data + /// (e.g., passwords). Instead, use + /// + /// for secure hashing instead. + /// + /// + /// See also: + /// + /// string GetSHA256Hash(byte[] buffer); /// - /// Computes the SHA384 hash value for the specified byte array. + /// Computes the SHA384 hash value for the specified . /// /// /// The input to compute the hash code for. /// /// - /// The hexadecimal representation of the computed hash code. + /// The hexadecimal representation of the computed hash code. /// - /// + /// + /// + /// WARNING: Do not use this method to compute hashes for confidential data + /// (e.g., passwords). Instead, use + /// + /// for secure hashing instead. + /// + /// + /// See also: + /// + /// string GetSHA384Hash(byte[] buffer); /// - /// Computes the SHA512 hash value for the specified byte array. + /// Computes the SHA512 hash value for the specified . /// /// /// The input to compute the hash code for. /// /// - /// The hexadecimal representation of the computed hash code. + /// The hexadecimal representation of the computed hash code. /// - /// + /// + /// + /// WARNING: Do not use this method to compute hashes for confidential data + /// (e.g., passwords). Instead, use + /// + /// for secure hashing instead. + /// + /// + /// See also: + /// + /// string GetSHA512Hash(byte[] buffer); + /// + /// Creates a cryptographically secure hash value for the specified + /// as a PBKDF2 derived key using the SHA1 hash algorithm. + /// + /// + /// The buffer used to derive the hash. + /// + /// + /// The salt used to derive the hash. + /// + /// + /// The number of iterations for the operation. + /// + /// + /// The hexadecimal representation of the computed hash code + /// if the computation succeeded; else . + /// + /// + /// Notes on usage: + /// + /// + /// Create an array of bytes filled with cryptographically strong random sequence + /// of values for the parameter. + /// + /// + /// The has to be stored alongside the password hash and + /// count. + /// + /// + /// A higher count results in more computational + /// overhead, thus slowing down this function invocation considerably. This behavior + /// is intentional to mitigate brute-force attacks on leaked databases. + /// + /// + /// See also: . + /// + public string GetSecureSHA1Hash(byte[] buffer, byte[] salt, int iterations = 1000); + + /// + /// Creates a cryptographically secure hash value for the specified + /// as a PBKDF2 derived key using the SHA256 hash algorithm. + /// + /// + /// The buffer used to derive the hash. + /// + /// + /// The salt used to derive the hash. + /// + /// + /// The number of iterations for the operation. + /// + /// + /// The hexadecimal representation of the computed hash code + /// if the computation succeeded; else . + /// + /// + /// + /// + public string GetSecureSHA256Hash(byte[] buffer, byte[] salt, int iterations = 1000); + + /// + /// Creates a cryptographically secure hash value for the specified + /// as a PBKDF2 derived key using the SHA384 hash algorithm. + /// + /// + /// The buffer used to derive the hash. + /// + /// + /// The salt used to derive the hash. + /// + /// + /// The number of iterations for the operation. + /// + /// + /// The hexadecimal representation of the computed hash code + /// if the computation succeeded; else . + /// + /// + /// + /// + public string GetSecureSHA384Hash(byte[] buffer, byte[] salt, int iterations = 1000); + + /// + /// Creates a cryptographically secure hash value for the specified + /// as a PBKDF2 derived key using the SHA512 hash algorithm. + /// + /// + /// The buffer used to derive the hash. + /// + /// + /// The salt used to derive the hash. + /// + /// + /// The number of iterations for the operation. + /// + /// + /// The hexadecimal representation of the computed hash code + /// if the computation succeeded; else . + /// + /// + /// + /// + public string GetSecureSHA512Hash(byte[] buffer, byte[] salt, int iterations = 1000); + #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security/Cryptography/Hash.cs b/AdvancedSystems.Security/Cryptography/Hash.cs index 394e34e..d94d247 100644 --- a/AdvancedSystems.Security/Cryptography/Hash.cs +++ b/AdvancedSystems.Security/Cryptography/Hash.cs @@ -3,6 +3,8 @@ using System.Runtime.CompilerServices; using System.Security.Cryptography; +using AdvancedSystems.Security.Abstractions; + namespace AdvancedSystems.Security.Cryptography; /// @@ -53,7 +55,6 @@ public static HashAlgorithm Create(HashAlgorithmName hashAlgorithmName) public static byte[] Compute(byte[] buffer, HashAlgorithmName hashAlgorithmName) { using var hashAlgorithm = Hash.Create(hashAlgorithmName); - return hashAlgorithm.ComputeHash(buffer); } @@ -79,13 +80,13 @@ public static byte[] Compute(byte[] buffer, HashAlgorithmName hashAlgorithmName) /// /// /// - /// + /// /// /// - /// + /// /// /// - /// + /// /// /// /// @@ -96,19 +97,7 @@ public static byte[] Compute(byte[] buffer, HashAlgorithmName hashAlgorithmName) /// if the operation succeeds; otherwise, . /// /// - /// Notes on usage: - /// - /// - /// The has to be stored alongside the password hash and - /// count. - /// - /// - /// A higher count results in more computational - /// overhead, thus slowing down this function invocation considerably. This behavior - /// is intentional to mitigate brute-force attacks on leaked databases. - /// - /// - /// See also: . + /// /// public static bool TryComputeSecure(byte[] password, byte[] salt, int hashSize, int iterations, HashAlgorithmName hashAlgorithmName, [NotNullWhen(true)] out byte[] pbkdf2) { diff --git a/AdvancedSystems.Security/Services/HashService.cs b/AdvancedSystems.Security/Services/HashService.cs index bd3186c..d7effb4 100644 --- a/AdvancedSystems.Security/Services/HashService.cs +++ b/AdvancedSystems.Security/Services/HashService.cs @@ -60,5 +60,41 @@ public string GetSHA512Hash(byte[] buffer) return sha512.ToString(Format.Hex); } + /// + public string GetSecureSHA1Hash(byte[] buffer, byte[] salt, int iterations = 1000) + { + var hashAlgorithmName = HashAlgorithmName.SHA1; + using var hashAlgorithm = Hash.Create(hashAlgorithmName); + bool isSuccessful = Hash.TryComputeSecure(buffer, salt, hashAlgorithm.HashSize, iterations, hashAlgorithmName, out byte[] sha1); + return isSuccessful ? sha1.ToString(Format.Hex) : string.Empty; + } + + /// + public string GetSecureSHA256Hash(byte[] buffer, byte[] salt, int iterations = 1000) + { + var hashAlgorithmName = HashAlgorithmName.SHA256; + using var hashAlgorithm = Hash.Create(hashAlgorithmName); + bool isSuccessful = Hash.TryComputeSecure(buffer, salt, hashAlgorithm.HashSize, iterations, hashAlgorithmName, out byte[] sha256); + return isSuccessful ? sha256.ToString(Format.Hex) : string.Empty; + } + + /// + public string GetSecureSHA384Hash(byte[] buffer, byte[] salt, int iterations = 1000) + { + var hashAlgorithmName = HashAlgorithmName.SHA384; + using var hashAlgorithm = Hash.Create(hashAlgorithmName); + bool isSuccessful = Hash.TryComputeSecure(buffer, salt, hashAlgorithm.HashSize, iterations, hashAlgorithmName, out byte[] sha384); + return isSuccessful ? sha384.ToString(Format.Hex) : string.Empty; + } + + /// + public string GetSecureSHA512Hash(byte[] buffer, byte[] salt, int iterations = 1000) + { + var hashAlgorithmName = HashAlgorithmName.SHA512; + using var hashAlgorithm = Hash.Create(hashAlgorithmName); + bool isSuccessful = Hash.TryComputeSecure(buffer, salt, hashAlgorithm.HashSize, iterations, hashAlgorithmName, out byte[] sha512); + return isSuccessful ? sha512.ToString(Format.Hex) : string.Empty; + } + #endregion } \ No newline at end of file From eba07913d0cb674da1197f48b5256e26d6060f08 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 18 Jan 2025 16:34:29 +0100 Subject: [PATCH 26/41] Add unit tests for computing secure hashes in service and provider --- .../ICertificateStore.cs | 1 - .../Cryptography/HashTests.cs | 108 ++++++++++++++++++ .../Services/HashServiceTests.cs | 99 ++++++++++++++++ .../Services/HashService.cs | 2 +- 4 files changed, 208 insertions(+), 2 deletions(-) diff --git a/AdvancedSystems.Security.Abstractions/ICertificateStore.cs b/AdvancedSystems.Security.Abstractions/ICertificateStore.cs index d9b3b72..1c9f41e 100644 --- a/AdvancedSystems.Security.Abstractions/ICertificateStore.cs +++ b/AdvancedSystems.Security.Abstractions/ICertificateStore.cs @@ -2,7 +2,6 @@ using System.Security; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Security.Permissions; namespace AdvancedSystems.Security.Abstractions; diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs index 6c5bc07..90791ad 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs @@ -171,5 +171,113 @@ public void TestSHA512Hash(string input, string expected, Format format) Assert.Equal(expected, sha512); } + /// + /// Tests that + /// computes the hash successfully and returns the hash with the expected size using the SHA1 algorithm. + /// + [Fact] + public void TestTryGetSecureSHA1() + { + // Arrange + int iterations = 100_000; + const int SALT_SIZE = 64; + const int HASH_SIZE = 128; + + byte[] password = Encoding.UTF8.GetBytes("secret"); + byte[] salt = CryptoRandomProvider.GetBytes(SALT_SIZE).ToArray(); + + // Act + bool isSuccessful = Hash.TryComputeSecure(password, salt, HASH_SIZE, iterations, HashAlgorithmName.SHA1, out byte[] sha1); + + // Assert + Assert.Multiple(() => + { + Assert.True(isSuccessful); + Assert.Equal(SALT_SIZE, salt.Length); + Assert.Equal(HASH_SIZE, sha1.Length); + }); + } + + /// + /// Tests that + /// computes the hash successfully and returns the hash with the expected size using the SHA256 algorithm. + /// + [Fact] + public void TestTryGetSecureSHA256() + { + // Arrange + int iterations = 100_000; + const int SALT_SIZE = 64; + const int HASH_SIZE = 128; + + byte[] password = Encoding.UTF8.GetBytes("secret"); + byte[] salt = CryptoRandomProvider.GetBytes(SALT_SIZE).ToArray(); + + // Act + bool isSuccessful = Hash.TryComputeSecure(password, salt, HASH_SIZE, iterations, HashAlgorithmName.SHA256, out byte[] sha256); + + // Assert + Assert.Multiple(() => + { + Assert.True(isSuccessful); + Assert.Equal(SALT_SIZE, salt.Length); + Assert.Equal(HASH_SIZE, sha256.Length); + }); + } + + /// + /// Tests that + /// computes the hash successfully and returns the hash with the expected size using the SHA384 algorithm. + /// + [Fact] + public void TestTryGetSecureSHA384() + { + // Arrange + int iterations = 100_000; + const int SALT_SIZE = 64; + const int HASH_SIZE = 128; + + byte[] password = Encoding.UTF8.GetBytes("secret"); + byte[] salt = CryptoRandomProvider.GetBytes(SALT_SIZE).ToArray(); + + // Act + bool isSuccessful = Hash.TryComputeSecure(password, salt, HASH_SIZE, iterations, HashAlgorithmName.SHA384, out byte[] sha384); + + // Assert + Assert.Multiple(() => + { + Assert.True(isSuccessful); + Assert.Equal(SALT_SIZE, salt.Length); + Assert.Equal(HASH_SIZE, sha384.Length); + }); + } + + /// + /// Tests that + /// computes the hash successfully and returns the hash with the expected size using the SHA512 algorithm. + /// + [Fact] + public void TestTryGetSecureSHA512() + { + // Arrange + int iterations = 100_000; + const int SALT_SIZE = 64; + const int HASH_SIZE = 128; + + byte[] password = Encoding.UTF8.GetBytes("secret"); + byte[] salt = CryptoRandomProvider.GetBytes(SALT_SIZE).ToArray(); + + // Act + bool isSuccessful = Hash.TryComputeSecure(password, salt, HASH_SIZE, iterations, HashAlgorithmName.SHA512, out byte[] sha512); + + // Assert + Assert.Multiple(() => + { + Assert.True(isSuccessful); + Assert.Equal(SALT_SIZE, salt.Length); + Assert.Equal(HASH_SIZE, sha512.Length); + }); + } + #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs index 243a4c5..f2c9553 100644 --- a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs @@ -2,6 +2,7 @@ using System.Text; using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Tests.Fixtures; using Microsoft.Extensions.Logging; @@ -39,6 +40,7 @@ public void TestMD5Hash() // Arrange string input = "The quick brown fox jumps over the lazy dog"; byte[] buffer = Encoding.UTF8.GetBytes(input); + this._sut.Logger.Invocations.Clear(); // Act #pragma warning disable CS0618 // Type or member is obsolete @@ -66,6 +68,7 @@ public void TestSHA1Hash() // Arrange string input = "The quick brown fox jumps over the lazy dog"; byte[] buffer = Encoding.UTF8.GetBytes(input); + this._sut.Logger.Invocations.Clear(); // Act #pragma warning disable CS0618 // Type or member is obsolete @@ -134,5 +137,101 @@ public void TestSHA512Hash() Assert.Equal("07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6", sha512); } + /// + /// Tests that returns the hash + /// with the expected length. + /// + [Fact] + public void TestGetSecureSHA1Hash() + { + // Arrange + int sha1Size = 160; + int expectedLength = sha1Size * 2; + byte[] password = Encoding.UTF8.GetBytes("secret"); + byte[] salt = CryptoRandomProvider.GetBytes(32).ToArray(); + + // Act + string sha1 = this._sut.HashService.GetSecureSHA1Hash(password, salt); + + // Assert + Assert.Multiple(() => + { + Assert.NotEmpty(sha1); + Assert.Equal(expectedLength, sha1.Length); + }); + } + + /// + /// Tests that returns the hash + /// with the expected length. + /// + [Fact] + public void GetSecureSHA256Hash() + { + // Arrange + int sha256Size = 256; + int expectedLength = sha256Size * 2; + byte[] password = Encoding.UTF8.GetBytes("secret"); + byte[] salt = CryptoRandomProvider.GetBytes(32).ToArray(); + + // Act + string sha256 = this._sut.HashService.GetSecureSHA256Hash(password, salt); + + // Assert + Assert.Multiple(() => + { + Assert.NotEmpty(sha256); + Assert.Equal(expectedLength, sha256.Length); + }); + } + + /// + /// Tests that returns the hash + /// with the expected length. + /// + [Fact] + public void GetSecureSHA384Hash() + { + // Arrange + int sha384Size = 256; + int expectedLength = sha384Size * 2; + byte[] password = Encoding.UTF8.GetBytes("secret"); + byte[] salt = CryptoRandomProvider.GetBytes(32).ToArray(); + + // Act + string sha384 = this._sut.HashService.GetSecureSHA256Hash(password, salt); + + // Assert + Assert.Multiple(() => + { + Assert.NotEmpty(sha384); + Assert.Equal(expectedLength, sha384.Length); + }); + } + + /// + /// Tests that returns the hash + /// with the expected length. + /// + [Fact] + public void GetSecureSHA512Hash() + { + // Arrange + int sha512Size = 256; + int expectedLength = sha512Size * 2; + byte[] password = Encoding.UTF8.GetBytes("secret"); + byte[] salt = CryptoRandomProvider.GetBytes(32).ToArray(); + + // Act + string sha512 = this._sut.HashService.GetSecureSHA256Hash(password, salt); + + // Assert + Assert.Multiple(() => + { + Assert.NotEmpty(sha512); + Assert.Equal(expectedLength, sha512.Length); + }); + } + #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security/Services/HashService.cs b/AdvancedSystems.Security/Services/HashService.cs index d7effb4..e89ac3a 100644 --- a/AdvancedSystems.Security/Services/HashService.cs +++ b/AdvancedSystems.Security/Services/HashService.cs @@ -33,7 +33,7 @@ public string GetMD5Hash(byte[] buffer) /// public string GetSHA1Hash(byte[] buffer) { - this._logger.LogWarning("Computing hash with a cryptographically insecure hash algorithm (SHA1.)"); + this._logger.LogWarning("Computing hash with a cryptographically insecure hash algorithm (SHA1)."); byte[] sha1 = Hash.Compute(buffer, HashAlgorithmName.SHA1); return sha1.ToString(Format.Hex); From 012144d1dc6189fb301308871ec84feada542c9f Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 26 Jan 2025 21:04:14 +0100 Subject: [PATCH 27/41] Refactor hash computation code --- .../HashFunction.cs | 13 ++ .../ICertificateService.cs | 6 +- .../ICertificateStore.cs | 2 +- .../ICryptoRandomService.cs | 3 + .../IHMACService.cs | 33 +++ .../IHashService.cs | 208 +++-------------- .../IRSACryptoService.cs | 13 +- .../Cryptography/HMACTests.cs | 119 ++++++++++ .../Cryptography/HashTests.cs | 145 +++++------- .../ServiceCollectionExtensionsTests.cs | 172 ++++++++------ .../Services/HashServiceTests.cs | 212 ++---------------- .../Cryptography/CryptoRandomProvider.cs | 3 + .../{Common => Cryptography}/Format.cs | 2 +- .../Cryptography/HMACProvider.cs | 54 +++++ AdvancedSystems.Security/Cryptography/Hash.cs | 115 ---------- .../Cryptography/HashProvider.cs | 70 ++++++ .../Cryptography/RSACryptoProvider.cs | 24 +- .../ServiceCollectionExtensions.cs | 21 +- AdvancedSystems.Security/Extensions/Bytes.cs | 2 +- .../Extensions/HashFunctionExtensions.cs | 93 ++++++++ .../Services/CertificateService.cs | 7 +- .../Services/CertificateStore.cs | 4 +- .../Services/CryptoRandomService.cs | 4 +- .../Services/HMACService.cs | 20 ++ .../Services/HashService.cs | 84 ++----- .../Services/RSACryptoService.cs | 12 +- docs/docs/providers.md | 19 ++ docs/docs/services.md | 23 ++ docs/docs/toc.yml | 4 + 29 files changed, 733 insertions(+), 754 deletions(-) create mode 100644 AdvancedSystems.Security.Abstractions/HashFunction.cs create mode 100644 AdvancedSystems.Security.Abstractions/IHMACService.cs create mode 100644 AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs rename AdvancedSystems.Security/{Common => Cryptography}/Format.cs (92%) create mode 100644 AdvancedSystems.Security/Cryptography/HMACProvider.cs delete mode 100644 AdvancedSystems.Security/Cryptography/Hash.cs create mode 100644 AdvancedSystems.Security/Cryptography/HashProvider.cs create mode 100644 AdvancedSystems.Security/Extensions/HashFunctionExtensions.cs create mode 100644 AdvancedSystems.Security/Services/HMACService.cs create mode 100644 docs/docs/providers.md create mode 100644 docs/docs/services.md diff --git a/AdvancedSystems.Security.Abstractions/HashFunction.cs b/AdvancedSystems.Security.Abstractions/HashFunction.cs new file mode 100644 index 0000000..cebd2af --- /dev/null +++ b/AdvancedSystems.Security.Abstractions/HashFunction.cs @@ -0,0 +1,13 @@ +namespace AdvancedSystems.Security.Abstractions; + +public enum HashFunction +{ + MD5 = 0, + SHA1 = 1, + SHA256 = 2, + SHA384 = 3, + SHA512 = 4, + SHA3_256 = 5, + SHA3_384 = 6, + SHA3_512 = 7, +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Abstractions/ICertificateService.cs b/AdvancedSystems.Security.Abstractions/ICertificateService.cs index f39af28..d122942 100644 --- a/AdvancedSystems.Security.Abstractions/ICertificateService.cs +++ b/AdvancedSystems.Security.Abstractions/ICertificateService.cs @@ -4,9 +4,11 @@ namespace AdvancedSystems.Security.Abstractions; /// -/// Defines a service for managing and retrieving X.509 certificates. +/// Defines a contract for managing and retrieving X.509 certificates. /// -/// +/// +/// See also: . +/// /// public interface ICertificateService { diff --git a/AdvancedSystems.Security.Abstractions/ICertificateStore.cs b/AdvancedSystems.Security.Abstractions/ICertificateStore.cs index 1c9f41e..6f158f9 100644 --- a/AdvancedSystems.Security.Abstractions/ICertificateStore.cs +++ b/AdvancedSystems.Security.Abstractions/ICertificateStore.cs @@ -6,7 +6,7 @@ namespace AdvancedSystems.Security.Abstractions; /// -/// Represents an X.509 store, which is a physical store where certificates are persisted and managed. +/// Represents a contract for an X.509 store, which is a physical store where certificates are persisted and managed. /// /// public interface ICertificateStore : IDisposable diff --git a/AdvancedSystems.Security.Abstractions/ICryptoRandomService.cs b/AdvancedSystems.Security.Abstractions/ICryptoRandomService.cs index 2de1888..0f95bb9 100644 --- a/AdvancedSystems.Security.Abstractions/ICryptoRandomService.cs +++ b/AdvancedSystems.Security.Abstractions/ICryptoRandomService.cs @@ -2,6 +2,9 @@ namespace AdvancedSystems.Security.Abstractions; +/// +/// Represents a contract for performing cryptographically secure numerical operations. +/// public interface ICryptoRandomService { #region Methods diff --git a/AdvancedSystems.Security.Abstractions/IHMACService.cs b/AdvancedSystems.Security.Abstractions/IHMACService.cs new file mode 100644 index 0000000..67acdb1 --- /dev/null +++ b/AdvancedSystems.Security.Abstractions/IHMACService.cs @@ -0,0 +1,33 @@ +using System; + +namespace AdvancedSystems.Security.Abstractions; + +/// +/// Represents a contract designed for computing Hash-Based Message Authentication Codes (HMAC). +/// +public interface IHMACService +{ + #region Methods + + /// + /// Computes the hash value for the specified byte array using the specified . + /// + /// + /// The input to compute the hash code for. + /// + /// + /// The secret key for HMAC computation. + /// + /// + /// The hash algorithm implementation to use. + /// + /// + /// The computed hash code. + /// + /// + /// Raised if the specified is not implemented. + /// + public byte[] Compute(byte[] buffer, byte[] key, HashFunction hashFunction); + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Abstractions/IHashService.cs b/AdvancedSystems.Security.Abstractions/IHashService.cs index c302956..794d0a9 100644 --- a/AdvancedSystems.Security.Abstractions/IHashService.cs +++ b/AdvancedSystems.Security.Abstractions/IHashService.cs @@ -3,222 +3,72 @@ namespace AdvancedSystems.Security.Abstractions; /// -/// Defines a service for computing hash codes. +/// Represents a contract designed for computing hash algorithms. /// public interface IHashService { #region Methods /// - /// Computes the MD5 hash value for the specified . + /// Computes the hash value for the specified byte array. /// /// /// The input to compute the hash code for. /// - /// - /// The hexadecimal representation of the computed hash code. - /// - /// - /// - /// See also: - /// - /// - [Obsolete("MD5 is not a cryptographically secure hash algorithm.")] - string GetMD5Hash(byte[] buffer); - - /// - /// Computes the SHA1 hash value for the specified . - /// - /// - /// The input to compute the hash code for. - /// - /// - /// The hexadecimal representation of the computed hash code. - /// - /// - /// - /// WARNING: Do not use this method to compute hashes for confidential data - /// (e.g., passwords). Instead, use - /// - /// for secure hashing instead. - /// - /// - /// See also: - /// - /// - /// - [Obsolete("SHA1 is not a cryptographically secure hash algorithm.")] - string GetSHA1Hash(byte[] buffer); - - /// - /// Computes the SHA256 hash value for the specified . - /// - /// - /// The input to compute the hash code for. - /// - /// - /// The hexadecimal representation of the computed hash code. - /// - /// - /// - /// WARNING: Do not use this method to compute hashes for confidential data - /// (e.g., passwords). Instead, use - /// - /// for secure hashing instead. - /// - /// - /// See also: - /// - /// - string GetSHA256Hash(byte[] buffer); - - /// - /// Computes the SHA384 hash value for the specified . - /// - /// - /// The input to compute the hash code for. - /// - /// - /// The hexadecimal representation of the computed hash code. - /// - /// - /// - /// WARNING: Do not use this method to compute hashes for confidential data - /// (e.g., passwords). Instead, use - /// - /// for secure hashing instead. - /// - /// - /// See also: - /// - /// - string GetSHA384Hash(byte[] buffer); - - /// - /// Computes the SHA512 hash value for the specified . - /// - /// - /// The input to compute the hash code for. + /// + /// The hash algorithm implementation to use. /// /// - /// The hexadecimal representation of the computed hash code. + /// The computed hash code. /// /// - /// - /// WARNING: Do not use this method to compute hashes for confidential data - /// (e.g., passwords). Instead, use - /// - /// for secure hashing instead. - /// - /// - /// See also: - /// + /// WARNING: Do not use this method to compute hashes for confidential data (e.g., passwords). /// - string GetSHA512Hash(byte[] buffer); + /// + /// Raised if the specified is not implemented. + /// + byte[] Compute(byte[] buffer, HashFunction hashFunction); /// - /// Creates a cryptographically secure hash value for the specified - /// as a PBKDF2 derived key using the SHA1 hash algorithm. + /// Attempts to compute a PBKDF2 (password-based key derivation function). + /// This method is suitable for securely hashing passwords. /// - /// - /// The buffer used to derive the hash. + /// + /// The password used to derive the hash. /// /// /// The salt used to derive the hash. /// + /// + /// The size of hash to derive. + /// /// /// The number of iterations for the operation. /// - /// - /// The hexadecimal representation of the computed hash code - /// if the computation succeeded; else . - /// - /// - /// Notes on usage: + /// + /// The hash algorithm to use to derive the hash. Supported algorithms are: /// /// - /// Create an array of bytes filled with cryptographically strong random sequence - /// of values for the parameter. + /// /// /// - /// The has to be stored alongside the password hash and - /// count. + /// /// /// - /// A higher count results in more computational - /// overhead, thus slowing down this function invocation considerably. This behavior - /// is intentional to mitigate brute-force attacks on leaked databases. + /// + /// + /// + /// /// /// - /// See also: . - /// - public string GetSecureSHA1Hash(byte[] buffer, byte[] salt, int iterations = 1000); - - /// - /// Creates a cryptographically secure hash value for the specified - /// as a PBKDF2 derived key using the SHA256 hash algorithm. - /// - /// - /// The buffer used to derive the hash. - /// - /// - /// The salt used to derive the hash. - /// - /// - /// The number of iterations for the operation. - /// - /// - /// The hexadecimal representation of the computed hash code - /// if the computation succeeded; else . - /// - /// - /// - /// - public string GetSecureSHA256Hash(byte[] buffer, byte[] salt, int iterations = 1000); - - /// - /// Creates a cryptographically secure hash value for the specified - /// as a PBKDF2 derived key using the SHA384 hash algorithm. - /// - /// - /// The buffer used to derive the hash. /// - /// - /// The salt used to derive the hash. - /// - /// - /// The number of iterations for the operation. - /// - /// - /// The hexadecimal representation of the computed hash code - /// if the computation succeeded; else . - /// - /// - /// - /// - public string GetSecureSHA384Hash(byte[] buffer, byte[] salt, int iterations = 1000); - - /// - /// Creates a cryptographically secure hash value for the specified - /// as a PBKDF2 derived key using the SHA512 hash algorithm. - /// - /// - /// The buffer used to derive the hash. - /// - /// - /// The salt used to derive the hash. - /// - /// - /// The number of iterations for the operation. + /// + /// A byte array containing the created PBKDF2 derived hash. /// /// - /// The hexadecimal representation of the computed hash code - /// if the computation succeeded; else . + /// if the operation succeeds; otherwise, . /// - /// - /// - /// - public string GetSecureSHA512Hash(byte[] buffer, byte[] salt, int iterations = 1000); + bool TryComputePBKDF2(byte[] password, byte[] salt, int hashSize, int iterations, HashFunction hashFunction, out byte[] pbkdf2); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs b/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs index 9b435eb..28bda07 100644 --- a/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs +++ b/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs @@ -5,6 +5,9 @@ namespace AdvancedSystems.Security.Abstractions; +/// +/// Represents a contract for performing RSA-based asymmetric operations. +/// public interface IRSACryptoService : IDisposable { #region Properties @@ -23,15 +26,13 @@ public interface IRSACryptoService : IDisposable #region Methods - bool IsValidMessage(string message, RSAEncryptionPadding? padding, Encoding? encoding = null); + byte[] Encrypt(byte[] message); - string Encrypt(string message, Encoding? encoding = null); + byte[] Decrypt(byte[] cipher); - string Decrypt(string cipher, Encoding? encoding = null); + byte[] SignData(byte[] data); - string SignData(string data, Encoding? encoding = null); - - bool VerifyData(string data, string signature, Encoding? encoding = null); + bool VerifyData(byte[] data, byte[] signature); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs new file mode 100644 index 0000000..5a4afb5 --- /dev/null +++ b/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs @@ -0,0 +1,119 @@ +using System.Text; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; + +using Xunit; + +namespace AdvancedSystems.Security.Tests.Cryptography; + +/// +/// Tests the public methods in . +/// +public sealed class HMACTests +{ + #region Tests + + [Theory] + [InlineData("Hello, World!", "006255bea760447cfb4a2a83f8dcd78e", Format.Hex)] + [InlineData("Hello, World!", "AGJVvqdgRHz7SiqD+NzXjg==", Format.Base64)] + [InlineData("The quick brown fox jumps over the lazy dog", "39bbfdd604b4ce30df059be91ff1cfdc", Format.Hex)] + [InlineData("The quick brown fox jumps over the lazy dog", "Obv91gS0zjDfBZvpH/HP3A==", Format.Base64)] + public void TestMD5HMAC(string input, string expectedHash, Format format) + { + // Arrange + Encoding encoding = Encoding.UTF8; + byte[] buffer = encoding.GetBytes(input); + byte[] key = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + + // Act + byte[] hash = HMACProvider.Compute(buffer, key, HashFunction.MD5); + string md5 = hash.ToString(format); + + // Assert + Assert.Equal(expectedHash, md5); + } + + [Theory] + [InlineData("Hello, World!", "74337880adf8df0e331ba348e8a78ff3fbae4504", Format.Hex)] + [InlineData("Hello, World!", "dDN4gK343w4zG6NI6KeP8/uuRQQ=", Format.Base64)] + [InlineData("The quick brown fox jumps over the lazy dog", "6e5535b20cfe26e18df61b465029f379320108ff", Format.Hex)] + [InlineData("The quick brown fox jumps over the lazy dog", "blU1sgz+JuGN9htGUCnzeTIBCP8=", Format.Base64)] + public void TestSHA1HMAC(string input, string expectedHash, Format format) + { + // Arrange + Encoding encoding = Encoding.UTF8; + byte[] buffer = encoding.GetBytes(input); + byte[] key = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + + // Act + byte[] hash = HMACProvider.Compute(buffer, key, HashFunction.SHA1); + string sha1 = hash.ToString(format); + + // Assert + Assert.Equal(expectedHash, sha1); + } + + [Theory] + [InlineData("Hello, World!", "ac453c5917826e9a73ea7681c28b2156727432ac68477b144236b35b8507436c", Format.Hex)] + //[InlineData("Hello, World!", "8WReCbppz6naBwoshVnJ0MqxoR3sUQjazW4UHQ2w=", Format.Base64)] + //[InlineData("The quick brown fox jumps over the lazy dog", "b1d5f845047ccaada44062c2ba3d4f9d24572e1fc", Format.Hex)] + //[InlineData("The quick brown fox jumps over the lazy dog", "Z39ZJXtPfbhRPKsdX4RQR8yq2kQGLCuj1PnSRXLh/", Format.Base64)] + public void TestSHA256HMAC(string input, string expectedHash, Format format) + { + // Arrange + Encoding encoding = Encoding.UTF8; + byte[] buffer = encoding.GetBytes(input); + byte[] key = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + + // Act + byte[] hash = HMACProvider.Compute(buffer, key, HashFunction.SHA256); + string sha256 = hash.ToString(format); + + // Assert + Assert.Equal(expectedHash, sha256); + } + + //[Theory] + //[InlineData("Hello, World!", "88474536c661377c09e2f81dac464b1ad3e4caeb9", Format.Hex)] + //[InlineData("Hello, World!", "AGJVvqdgRHz7SiqD+NzXjg==", Format.Base64)] + //[InlineData("The quick brown fox jumps over the lazy dog", "39bbfdd604b4ce30df059be91ff1cfdc", Format.Hex)] + //[InlineData("The quick brown fox jumps over the lazy dog", "Obv91gS0zjDfBZvpH/HP3A==", Format.Base64)] + //public void TestSHA384HMAC(string input, string expectedHash, Format format) + //{ + // // Arrange + // Encoding encoding = Encoding.UTF8; + // byte[] buffer = encoding.GetBytes(input); + // byte[] key = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + + // // Act + // byte[] hash = HMACProvider.Compute(buffer, key, HashFunction.SHA384); + // string sha384 = hash.ToString(format); + + // // Assert + // Assert.Equal(expectedHash, sha384); + //} + + //[Theory] + //[InlineData("Hello, World!", "c752bac8d1bcb622dddfded281b3cf2acf2c988c8", Format.Hex)] + //[InlineData("Hello, World!", "AGJVvqdgRHz7SiqD+NzXjg==", Format.Base64)] + //[InlineData("The quick brown fox jumps over the lazy dog", "39bbfdd604b4ce30df059be91ff1cfdc", Format.Hex)] + //[InlineData("The quick brown fox jumps over the lazy dog", "Obv91gS0zjDfBZvpH/HP3A==", Format.Base64)] + //public void TestSHA512HMAC(string input, string expectedHash, Format format) + //{ + // // Arrange + // Encoding encoding = Encoding.UTF8; + // byte[] buffer = encoding.GetBytes(input); + // byte[] key = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + + // // Act + // byte[] hash = HMACProvider.Compute(buffer, key, HashFunction.SHA512); + // string sha512 = hash.ToString(format); + + // // Assert + // Assert.Equal(expectedHash, sha512); + //} + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs index 90791ad..26b0d3a 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs @@ -1,7 +1,6 @@ -using System.Security.Cryptography; -using System.Text; +using System.Text; -using AdvancedSystems.Security.Common; +using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Extensions; @@ -10,14 +9,14 @@ namespace AdvancedSystems.Security.Tests.Cryptography; /// -/// Tests the public methods in . +/// Tests the public methods in . /// public sealed class HashTests { #region Tests /// - /// Tests that the computed hash returns a well-formatted string. + /// Tests that the computed hash returns a well-formatted string. /// /// /// The input to compute the hash code for. @@ -33,14 +32,14 @@ public sealed class HashTests [InlineData("Hello, World!", "ZajifYh5KDgxtmS9i38K1A==", Format.Base64)] [InlineData("The quick brown fox jumps over the lazy dog", "9e107d9d372bb6826bd81d3542a419d6", Format.Hex)] [InlineData("The quick brown fox jumps over the lazy dog", "nhB9nTcrtoJr2B01QqQZ1g==", Format.Base64)] - public void TestMd5Hash(string input, string expected, Format format) + public void TestMD5Hash(string input, string expected, Format format) { // Arrange Encoding encoding = Encoding.UTF8; byte[] buffer = encoding.GetBytes(input); // Act - byte[] hash = Hash.Compute(buffer, HashAlgorithmName.MD5); + byte[] hash = HashProvider.Compute(buffer, HashFunction.MD5); string md5 = hash.ToString(format); // Assert @@ -48,7 +47,7 @@ public void TestMd5Hash(string input, string expected, Format format) } /// - /// Tests that the computed hash returns a well-formatted string. + /// Tests that the computed hash returns a well-formatted string. /// /// /// The input to compute the hash code for. @@ -71,7 +70,7 @@ public void TestSHA1Hash(string input, string expected, Format format) byte[] buffer = encoding.GetBytes(input); // Act - byte[] hash = Hash.Compute(buffer, HashAlgorithmName.SHA1); + byte[] hash = HashProvider.Compute(buffer, HashFunction.SHA1); string sha1 = hash.ToString(format); // Assert @@ -79,7 +78,7 @@ public void TestSHA1Hash(string input, string expected, Format format) } /// - /// Tests that the computed hash returns a well-formatted string. + /// Tests that the computed hash returns a well-formatted string. /// /// /// The input to compute the hash code for. @@ -102,7 +101,7 @@ public void TestSHA256Hash(string input, string expected, Format format) byte[] buffer = encoding.GetBytes(input); // Act - byte[] hash = Hash.Compute(buffer, HashAlgorithmName.SHA256); + byte[] hash = HashProvider.Compute(buffer, HashFunction.SHA256); string sha256 = hash.ToString(format); // Assert @@ -110,7 +109,7 @@ public void TestSHA256Hash(string input, string expected, Format format) } /// - /// Tests that the computed hash returns a well-formatted string. + /// Tests that the computed hash returns a well-formatted string. /// /// /// The input to compute the hash code for. @@ -133,7 +132,7 @@ public void TestSHA384Hash(string input, string expected, Format format) byte[] buffer = encoding.GetBytes(input); // Act - byte[] hash = Hash.Compute(buffer, HashAlgorithmName.SHA384); + byte[] hash = HashProvider.Compute(buffer, HashFunction.SHA384); string sha384 = hash.ToString(format); // Assert @@ -141,7 +140,7 @@ public void TestSHA384Hash(string input, string expected, Format format) } /// - /// Tests that the computed hash returns a well-formatted string. + /// Tests that the computed hash returns a well-formatted string. /// /// /// The input to compute the hash code for. @@ -164,7 +163,7 @@ public void TestSHA512Hash(string input, string expected, Format format) byte[] buffer = encoding.GetBytes(input); // Act - byte[] hash = Hash.Compute(buffer, HashAlgorithmName.SHA512); + byte[] hash = HashProvider.Compute(buffer, HashFunction.SHA512); string sha512 = hash.ToString(format); // Assert @@ -172,110 +171,76 @@ public void TestSHA512Hash(string input, string expected, Format format) } /// - /// Tests that - /// computes the hash successfully and returns the hash with the expected size using the SHA1 algorithm. + /// Tests that + /// computes the hash code successfully and returns the hash with the expected size using the + /// algorithm. /// - [Fact] - public void TestTryGetSecureSHA1() - { - // Arrange - int iterations = 100_000; - const int SALT_SIZE = 64; - const int HASH_SIZE = 128; - - byte[] password = Encoding.UTF8.GetBytes("secret"); - byte[] salt = CryptoRandomProvider.GetBytes(SALT_SIZE).ToArray(); - - // Act - bool isSuccessful = Hash.TryComputeSecure(password, salt, HASH_SIZE, iterations, HashAlgorithmName.SHA1, out byte[] sha1); - - // Assert - Assert.Multiple(() => - { - Assert.True(isSuccessful); - Assert.Equal(SALT_SIZE, salt.Length); - Assert.Equal(HASH_SIZE, sha1.Length); - }); - } - - /// - /// Tests that - /// computes the hash successfully and returns the hash with the expected size using the SHA256 algorithm. - /// - [Fact] - public void TestTryGetSecureSHA256() - { - // Arrange - int iterations = 100_000; - const int SALT_SIZE = 64; - const int HASH_SIZE = 128; - - byte[] password = Encoding.UTF8.GetBytes("secret"); - byte[] salt = CryptoRandomProvider.GetBytes(SALT_SIZE).ToArray(); - - // Act - bool isSuccessful = Hash.TryComputeSecure(password, salt, HASH_SIZE, iterations, HashAlgorithmName.SHA256, out byte[] sha256); - - // Assert - Assert.Multiple(() => - { - Assert.True(isSuccessful); - Assert.Equal(SALT_SIZE, salt.Length); - Assert.Equal(HASH_SIZE, sha256.Length); - }); - } - - /// - /// Tests that - /// computes the hash successfully and returns the hash with the expected size using the SHA384 algorithm. - /// - [Fact] - public void TestTryGetSecureSHA384() + /// + /// The specified hash function. + /// + /// + /// The salt size to use. + /// + [Theory] + [InlineData(HashFunction.SHA1, 128)] + [InlineData(HashFunction.SHA256, 128)] + [InlineData(HashFunction.SHA384, 128)] + [InlineData(HashFunction.SHA512, 128)] + public void TestTryComputePBKDF2(HashFunction hashFunction, int saltSize) { // Arrange int iterations = 100_000; - const int SALT_SIZE = 64; - const int HASH_SIZE = 128; + int hashSize = hashFunction.GetSize(); byte[] password = Encoding.UTF8.GetBytes("secret"); - byte[] salt = CryptoRandomProvider.GetBytes(SALT_SIZE).ToArray(); + byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); // Act - bool isSuccessful = Hash.TryComputeSecure(password, salt, HASH_SIZE, iterations, HashAlgorithmName.SHA384, out byte[] sha384); + bool isSuccessful = HashProvider.TryComputePBKDF2(password, salt, hashSize, iterations, hashFunction, out byte[] hash); // Assert Assert.Multiple(() => { Assert.True(isSuccessful); - Assert.Equal(SALT_SIZE, salt.Length); - Assert.Equal(HASH_SIZE, sha384.Length); + Assert.Equal(saltSize, salt.Length); + Assert.Equal(hashSize, hash.Length); }); } /// - /// Tests that - /// computes the hash successfully and returns the hash with the expected size using the SHA512 algorithm. + /// Tests that + /// fails to compute the hash code on unsupported values and that the resulting + /// hash is empty. /// - [Fact] - public void TestTryGetSecureSHA512() + /// + /// The specified hash function. + /// + /// + /// The salt size to use. + /// + [Theory] + [InlineData(HashFunction.MD5, 128)] + [InlineData(HashFunction.SHA3_256, 128)] + [InlineData(HashFunction.SHA3_384, 128)] + [InlineData(HashFunction.SHA3_512, 128)] + public void TestTryComputePBKDF2_InvalidHashFunctions(HashFunction hashFunction, int saltSize) { // Arrange int iterations = 100_000; - const int SALT_SIZE = 64; - const int HASH_SIZE = 128; + int hashSize = hashFunction.GetSize(); byte[] password = Encoding.UTF8.GetBytes("secret"); - byte[] salt = CryptoRandomProvider.GetBytes(SALT_SIZE).ToArray(); + byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); // Act - bool isSuccessful = Hash.TryComputeSecure(password, salt, HASH_SIZE, iterations, HashAlgorithmName.SHA512, out byte[] sha512); + bool isSuccessful = HashProvider.TryComputePBKDF2(password, salt, hashSize, iterations, hashFunction, out byte[] hash); // Assert Assert.Multiple(() => { - Assert.True(isSuccessful); - Assert.Equal(SALT_SIZE, salt.Length); - Assert.Equal(HASH_SIZE, sha512.Length); + Assert.False(isSuccessful); + Assert.Equal(saltSize, salt.Length); + Assert.Empty(hash); }); } diff --git a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index d16071c..d2ab84b 100644 --- a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -3,7 +3,9 @@ using System.Threading.Tasks; using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.DependencyInjection; +using AdvancedSystems.Security.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; @@ -20,46 +22,74 @@ namespace AdvancedSystems.Security.Tests.DependencyInjection; /// public sealed class ServiceCollectionExtensionsTests { - #region AddCertificateService Tests + #region AddCryptoRandomService Tests /// - /// Tests that can be initialized through dependency injection. + /// Tests that can be initialized through dependency injection. /// [Fact] - public async Task TestAddCertificateService_FromOptions() + public async Task TestAddCryptoRandomService() { // Arrange - string storeService = "my/CurrentUser"; - string thumbprint = "2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC"; + 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.AddCertificateStore(storeService, options => - { - options.Location = StoreLocation.CurrentUser; - options.Name = StoreName.My; - }); - - services.AddCertificateService(); + services.AddHashService(); }) - .Configure(app => - { + .Configure(app => + { - })) - .StartAsync(); + })) + .StartAsync(); + + string input = "The quick brown fox jumps over the lazy dog"; + byte[] buffer = Encoding.UTF8.GetBytes(input); // Act - var certificateService = hostBuilder.Services.GetService(); - var certificate = certificateService?.GetCertificate(storeService, thumbprint, validOnly: false); + var hashService = hostBuilder.Services.GetService(); + byte[]? sha256 = hashService?.Compute(buffer, HashFunction.SHA256); // Assert Assert.Multiple(() => { - Assert.NotNull(certificateService); - Assert.NotNull(certificate); + Assert.NotNull(hashService); + Assert.Equal("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", sha256?.ToString(Format.Hex)); }); await hostBuilder.StopAsync(); @@ -67,6 +97,40 @@ public async Task TestAddCertificateService_FromOptions() #endregion + #region AddHMACService Tests + + /// + /// Tests that can be initialized through dependency injection. + /// + [Fact] + public async Task TestAddHMACService() + { + // Arrange + using var hostBuilder = await new HostBuilder() + .ConfigureWebHost(builder => + { + builder.UseTestServer(); + builder.ConfigureServices(services => + { + services.AddHMACService(); + }); + builder.Configure(app => + { + + }); + }) + .StartAsync(); + + // Act + var cryptoRandomService = hostBuilder.Services.GetService(); + + // Assert + Assert.NotNull(cryptoRandomService); + await hostBuilder.StopAsync(); + } + + #endregion + #region AddCertificateStore Tests /// @@ -140,74 +204,46 @@ public async Task TestAddCertificateStore_FromAppSettings() #endregion - #region AddCryptoRandomService Tests + #region AddCertificateService Tests /// - /// Tests that can be initialized through dependency injection. + /// Tests that can be initialized through dependency injection. /// [Fact] - public async Task TestAddCryptoRandomService() + public async Task TestAddCertificateService_FromOptions() { // 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 + string storeService = "my/CurrentUser"; + string thumbprint = "2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC"; - /// - /// 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 => - { + services.AddCertificateStore(storeService, options => + { + options.Location = StoreLocation.CurrentUser; + options.Name = StoreName.My; + }); - })) - .StartAsync(); + services.AddCertificateService(); + }) + .Configure(app => + { - string input = "The quick brown fox jumps over the lazy dog"; - byte[] buffer = Encoding.UTF8.GetBytes(input); + })) + .StartAsync(); // Act - var hashService = hostBuilder.Services.GetService(); - string? sha256 = hashService?.GetSHA256Hash(buffer); + var certificateService = hostBuilder.Services.GetService(); + var certificate = certificateService?.GetCertificate(storeService, thumbprint, validOnly: false); // Assert Assert.Multiple(() => { - Assert.NotNull(hashService); - Assert.Equal("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", sha256); + Assert.NotNull(certificateService); + Assert.NotNull(certificate); }); await hostBuilder.StopAsync(); diff --git a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs index f2c9553..62c3694 100644 --- a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs @@ -3,6 +3,7 @@ using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; using AdvancedSystems.Security.Tests.Fixtures; using Microsoft.Extensions.Logging; @@ -31,206 +32,41 @@ public HashServiceTests(HashServiceFixture fixture) #region Tests /// - /// Tests that returns the expected hash, - /// and that the log warning message is called. + /// Tests that returns the expected hash, + /// and that the log warning message is called on or . /// - [Fact] - public void TestMD5Hash() + /// + /// + /// + [Theory] + [InlineData("The quick brown fox jumps over the lazy dog", "9e107d9d372bb6826bd81d3542a419d6", HashFunction.MD5)] + [InlineData("The quick brown fox jumps over the lazy dog", "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", HashFunction.SHA1)] + [InlineData("The quick brown fox jumps over the lazy dog", "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", HashFunction.SHA256)] + [InlineData("The quick brown fox jumps over the lazy dog", "ca737f1014a48f4c0b6dd43cb177b0afd9e5169367544c494011e3317dbf9a509cb1e5dc1e85a941bbee3d7f2afbc9b1", HashFunction.SHA384)] + [InlineData("The quick brown fox jumps over the lazy dog", "07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6", HashFunction.SHA512)] + public void TestCompute(string input, string expectedHash, HashFunction hashFunction) { // Arrange - string input = "The quick brown fox jumps over the lazy dog"; byte[] buffer = Encoding.UTF8.GetBytes(input); this._sut.Logger.Invocations.Clear(); // Act -#pragma warning disable CS0618 // Type or member is obsolete - string md5 = this._sut.HashService.GetMD5Hash(buffer); -#pragma warning restore CS0618 // Type or member is obsolete + string actualHash = this._sut.HashService.Compute(buffer, hashFunction).ToString(Format.Hex); // Assert - Assert.Equal("9e107d9d372bb6826bd81d3542a419d6", md5); - this._sut.Logger.Verify(x => x.Log( - LogLevel.Warning, - It.IsAny(), - It.Is((v, t) => v.ToString()!.StartsWith("Computing hash with a cryptographically insecure hash algorithm")), - It.IsAny(), - It.Is>((v, t) => true)) - ); - } - - /// - /// Tests that returns the expected hash, - /// and that the log warning message is called. - /// - [Fact] - public void TestSHA1Hash() - { - // Arrange - string input = "The quick brown fox jumps over the lazy dog"; - byte[] buffer = Encoding.UTF8.GetBytes(input); - this._sut.Logger.Invocations.Clear(); - - // Act -#pragma warning disable CS0618 // Type or member is obsolete - string sha1 = this._sut.HashService.GetSHA1Hash(buffer); -#pragma warning restore CS0618 // Type or member is obsolete - - // Assert - Assert.Equal("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", sha1); - this._sut.Logger.Verify(x => x.Log( - LogLevel.Warning, - It.IsAny(), - It.Is((v, t) => v.ToString()!.StartsWith("Computing hash with a cryptographically insecure hash algorithm")), - It.IsAny(), - It.Is>((v, t) => true)) - ); - } - - /// - /// Tests that returns the expected hash. - /// - [Fact] - public void TestSHA256Hash() - { - // Arrange - string input = "The quick brown fox jumps over the lazy dog"; - byte[] buffer = Encoding.UTF8.GetBytes(input); - - // Act - string sha256 = this._sut.HashService.GetSHA256Hash(buffer); - - // Assert - Assert.Equal("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", sha256); - } - - /// - /// Tests that returns the expected hash. - /// - [Fact] - public void TestSHA384Hash() - { - // Arrange - string input = "The quick brown fox jumps over the lazy dog"; - byte[] buffer = Encoding.UTF8.GetBytes(input); - - // Act - string sha384 = this._sut.HashService.GetSHA384Hash(buffer); - - // Assert - Assert.Equal("ca737f1014a48f4c0b6dd43cb177b0afd9e5169367544c494011e3317dbf9a509cb1e5dc1e85a941bbee3d7f2afbc9b1", sha384); - } - - /// - /// Tests that returns the expected hash. - /// - [Fact] - public void TestSHA512Hash() - { - // Arrange - string input = "The quick brown fox jumps over the lazy dog"; - byte[] buffer = Encoding.UTF8.GetBytes(input); - - // Act - string sha512 = this._sut.HashService.GetSHA512Hash(buffer); + Assert.Equal(expectedHash, actualHash); - // Assert - Assert.Equal("07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6", sha512); - } - - /// - /// Tests that returns the hash - /// with the expected length. - /// - [Fact] - public void TestGetSecureSHA1Hash() - { - // Arrange - int sha1Size = 160; - int expectedLength = sha1Size * 2; - byte[] password = Encoding.UTF8.GetBytes("secret"); - byte[] salt = CryptoRandomProvider.GetBytes(32).ToArray(); - - // Act - string sha1 = this._sut.HashService.GetSecureSHA1Hash(password, salt); - - // Assert - Assert.Multiple(() => + if (hashFunction is HashFunction.MD5 or HashFunction.SHA1) { - Assert.NotEmpty(sha1); - Assert.Equal(expectedLength, sha1.Length); - }); - } - - /// - /// Tests that returns the hash - /// with the expected length. - /// - [Fact] - public void GetSecureSHA256Hash() - { - // Arrange - int sha256Size = 256; - int expectedLength = sha256Size * 2; - byte[] password = Encoding.UTF8.GetBytes("secret"); - byte[] salt = CryptoRandomProvider.GetBytes(32).ToArray(); + this._sut.Logger.Verify(x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString()!.StartsWith("Computing hash with a cryptographically insecure hash algorithm")), + It.IsAny(), + It.Is>((v, t) => true)) + ); + } - // Act - string sha256 = this._sut.HashService.GetSecureSHA256Hash(password, salt); - - // Assert - Assert.Multiple(() => - { - Assert.NotEmpty(sha256); - Assert.Equal(expectedLength, sha256.Length); - }); - } - - /// - /// Tests that returns the hash - /// with the expected length. - /// - [Fact] - public void GetSecureSHA384Hash() - { - // Arrange - int sha384Size = 256; - int expectedLength = sha384Size * 2; - byte[] password = Encoding.UTF8.GetBytes("secret"); - byte[] salt = CryptoRandomProvider.GetBytes(32).ToArray(); - - // Act - string sha384 = this._sut.HashService.GetSecureSHA256Hash(password, salt); - - // Assert - Assert.Multiple(() => - { - Assert.NotEmpty(sha384); - Assert.Equal(expectedLength, sha384.Length); - }); - } - - /// - /// Tests that returns the hash - /// with the expected length. - /// - [Fact] - public void GetSecureSHA512Hash() - { - // Arrange - int sha512Size = 256; - int expectedLength = sha512Size * 2; - byte[] password = Encoding.UTF8.GetBytes("secret"); - byte[] salt = CryptoRandomProvider.GetBytes(32).ToArray(); - - // Act - string sha512 = this._sut.HashService.GetSecureSHA256Hash(password, salt); - - // Assert - Assert.Multiple(() => - { - Assert.NotEmpty(sha512); - Assert.Equal(expectedLength, sha512.Length); - }); } #endregion diff --git a/AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs b/AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs index f300911..2631b9b 100644 --- a/AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs +++ b/AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs @@ -6,6 +6,9 @@ namespace AdvancedSystems.Security.Cryptography; +/// +/// Represents a class for performing cryptographically secure numerical operations. +/// public static class CryptoRandomProvider { /// diff --git a/AdvancedSystems.Security/Common/Format.cs b/AdvancedSystems.Security/Cryptography/Format.cs similarity index 92% rename from AdvancedSystems.Security/Common/Format.cs rename to AdvancedSystems.Security/Cryptography/Format.cs index 0d4aaa0..ad34d1c 100644 --- a/AdvancedSystems.Security/Common/Format.cs +++ b/AdvancedSystems.Security/Cryptography/Format.cs @@ -1,4 +1,4 @@ -namespace AdvancedSystems.Security.Common; +namespace AdvancedSystems.Security.Cryptography; /// /// Describes string formatting options for byte arrays. diff --git a/AdvancedSystems.Security/Cryptography/HMACProvider.cs b/AdvancedSystems.Security/Cryptography/HMACProvider.cs new file mode 100644 index 0000000..934126b --- /dev/null +++ b/AdvancedSystems.Security/Cryptography/HMACProvider.cs @@ -0,0 +1,54 @@ +using System; +using System.Security.Cryptography; + +using AdvancedSystems.Security.Abstractions; + +namespace AdvancedSystems.Security.Cryptography; + +/// +/// Represents a class designed for computing HashProvider-Based Message Authentication Codes (HMACProvider). +/// +public static class HMACProvider +{ + /// + public static byte[] Compute(byte[] buffer, byte[] key, HashFunction hashFunction) + { + using var hmac = HMACProvider.Create(key, hashFunction); + return hmac.ComputeHash(buffer); + } + + #region Helpers + + /// + /// Creates a new instance of that implements . + /// + /// + /// The secret key for HMACProvider computation. + /// + /// + /// The hash function to use. + /// + /// + /// A new instance of . + /// + /// + /// Raised if the specified is not implemented. + /// + private static KeyedHashAlgorithm Create(byte[] key, HashFunction hashFunction) + { + return hashFunction switch + { + HashFunction.MD5 => new HMACMD5(key), + HashFunction.SHA1 => new HMACSHA1(key), + HashFunction.SHA256 => new HMACSHA256(key), + HashFunction.SHA384 => new HMACSHA384(key), + HashFunction.SHA512 => new HMACSHA512(key), + HashFunction.SHA3_256 => new HMACSHA3_256(key), + HashFunction.SHA3_384 => new HMACSHA3_384(key), + HashFunction.SHA3_512 => new HMACSHA3_512(key), + _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."), + }; + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Cryptography/Hash.cs b/AdvancedSystems.Security/Cryptography/Hash.cs deleted file mode 100644 index d94d247..0000000 --- a/AdvancedSystems.Security/Cryptography/Hash.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; - -using AdvancedSystems.Security.Abstractions; - -namespace AdvancedSystems.Security.Cryptography; - -/// -/// Implements cryptographic hash algorithms. -/// -public static class Hash -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static HashAlgorithm Create(HashAlgorithmName hashAlgorithmName) - { - // Starting with net8, cryptographic factory methods accepting an algorithm - // name are obsolet and to be replaced by the parameterless Create factory - // method on the algorithm type, because their derived cryptographic types - // such as SHA1Managed were obsoleted with net6. That is why this function - // is intentionally not using the HashAlgorithmName.Create(name) factory. - return hashAlgorithmName.Name switch - { - "MD5" => MD5.Create(), - "SHA1" => SHA1.Create(), - "SHA256" => SHA256.Create(), - "SHA384" => SHA384.Create(), - "SHA512" => SHA512.Create(), - "SHA3-256" => SHA3_256.Create(), - "SHA3-384" => SHA3_384.Create(), - "SHA3-512" => SHA3_512.Create(), - _ => throw new NotImplementedException() - }; - } - - /// - /// Computes the hash value for the specified byte array. - /// - /// - /// The input to compute the hash code for. - /// - /// - /// The hash algorithm implementation to use. - /// - /// - /// The computed hash code. - /// - /// - /// WARNING: Do not use this method to compute hashes for confidential data (e.g., passwords). - /// Instead, use - /// - /// for secure hashing. - /// - public static byte[] Compute(byte[] buffer, HashAlgorithmName hashAlgorithmName) - { - using var hashAlgorithm = Hash.Create(hashAlgorithmName); - return hashAlgorithm.ComputeHash(buffer); - } - - /// - /// Creates a cryptographically secure hash value for the specified byte array as a PBKDF2 derived key. - /// - /// - /// The password used to derive the hash. - /// - /// - /// The salt used to derive the hash. - /// - /// - /// The size of hash to derive. - /// - /// - /// The number of iterations for the operation. - /// - /// - /// The hash algorithm to use to derive the hash. Supported algorithms are: - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// A byte array containing the created PBKDF2 derived hash. - /// - /// - /// if the operation succeeds; otherwise, . - /// - /// - /// - /// - public static bool TryComputeSecure(byte[] password, byte[] salt, int hashSize, int iterations, HashAlgorithmName hashAlgorithmName, [NotNullWhen(true)] out byte[] pbkdf2) - { - try - { - pbkdf2 = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, hashAlgorithmName, hashSize); - return true; - } - catch (Exception) - { - pbkdf2 = []; - return false; - } - } -} \ No newline at end of file diff --git a/AdvancedSystems.Security/Cryptography/HashProvider.cs b/AdvancedSystems.Security/Cryptography/HashProvider.cs new file mode 100644 index 0000000..236e186 --- /dev/null +++ b/AdvancedSystems.Security/Cryptography/HashProvider.cs @@ -0,0 +1,70 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Extensions; + +namespace AdvancedSystems.Security.Cryptography; + +/// +/// Represents a class designed for computing hash algorithms. +/// +public static class HashProvider +{ + /// + public static byte[] Compute(byte[] buffer, HashFunction hashFunction) + { + using var hashAlgorithm = HashProvider.Create(hashFunction); + return hashAlgorithm.ComputeHash(buffer); + } + + /// + public static bool TryComputePBKDF2(byte[] password, byte[] salt, int hashSize, int iterations, HashFunction hashFunction, [NotNullWhen(true)] out byte[] pbkdf2) + { + try + { + pbkdf2 = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, hashFunction.ToHashAlgorithmName(), hashSize); + return true; + } + catch (Exception) + { + pbkdf2 = []; + return false; + } + } + + #region Helpers + + /// + /// Creates a new instance of that implements . + /// + /// + /// The hash function to use. + /// + /// + /// A new instance of . + /// + /// + /// Raised if the specified is not implemented. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static HashAlgorithm Create(HashFunction hashFunction) + { + return hashFunction switch + { + HashFunction.MD5 => MD5.Create(), + HashFunction.SHA1 => SHA1.Create(), + HashFunction.SHA256 => SHA256.Create(), + HashFunction.SHA384 => SHA384.Create(), + HashFunction.SHA512 => SHA512.Create(), + HashFunction.SHA3_256 => SHA3_256.Create(), + HashFunction.SHA3_384 => SHA3_384.Create(), + HashFunction.SHA3_512 => SHA3_512.Create(), + _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."), + }; + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs index 4db4394..656910e 100644 --- a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs +++ b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs @@ -7,6 +7,9 @@ namespace AdvancedSystems.Security.Cryptography; +/// +/// Represents a class for performing RSA-based asymmetric operations. +/// public sealed class RSACryptoProvider { private static readonly HashAlgorithmName DEFAULT_HASH_ALGORITHM_NAME = HashAlgorithmName.SHA256; @@ -48,27 +51,6 @@ public RSACryptoProvider(X509Certificate2 certificate) #region Public Methods - public bool IsValidMessage(string message, RSAEncryptionPadding? padding, Encoding? encoding = null) - { - using RSA? publicKey = this.Certificate.GetRSAPublicKey(); - ArgumentNullException.ThrowIfNull(publicKey, nameof(publicKey)); - - encoding ??= this.Encoding; - int messageSize = encoding.GetByteCount(message); - decimal keySize = Math.Floor(publicKey.KeySize / 8M); - - if (padding is null) - { - decimal limit = keySize - 11; - return messageSize <= limit; - } - - using var hashAlgorithm = Hash.Create(padding.OaepHashAlgorithm); - decimal hashSize = Math.Ceiling(hashAlgorithm.HashSize / 8M); - decimal limitPadded = keySize - (2 * hashSize) - 2; - return messageSize <= limitPadded; - } - public string Encrypt(string message, Encoding? encoding = null) { encoding ??= this.Encoding; diff --git a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs index 11e3c9a..2592db5 100644 --- a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,11 +1,9 @@ using System; -using System.Runtime.CompilerServices; using AdvancedSystems.Core.DependencyInjection; using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Options; using AdvancedSystems.Security.Services; -using AdvancedSystems.Security.Validators; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -54,6 +52,25 @@ public static IServiceCollection AddHashService(this IServiceCollection services #endregion + #region HMACService + + /// + /// Adds the default implementation of to . + /// + /// + /// The service collection containing the service. + /// + /// + /// The value of . + /// + public static IServiceCollection AddHMACService(this IServiceCollection services) + { + services.TryAdd(ServiceDescriptor.Scoped()); + return services; + } + + #endregion + #region CertificateStore private static IServiceCollection AddCertificateStore(this IServiceCollection services, string key) diff --git a/AdvancedSystems.Security/Extensions/Bytes.cs b/AdvancedSystems.Security/Extensions/Bytes.cs index bd4ed7c..86804be 100644 --- a/AdvancedSystems.Security/Extensions/Bytes.cs +++ b/AdvancedSystems.Security/Extensions/Bytes.cs @@ -1,7 +1,7 @@ using System; using System.Text; -using AdvancedSystems.Security.Common; +using AdvancedSystems.Security.Cryptography; namespace AdvancedSystems.Security.Extensions; diff --git a/AdvancedSystems.Security/Extensions/HashFunctionExtensions.cs b/AdvancedSystems.Security/Extensions/HashFunctionExtensions.cs new file mode 100644 index 0000000..f46eba4 --- /dev/null +++ b/AdvancedSystems.Security/Extensions/HashFunctionExtensions.cs @@ -0,0 +1,93 @@ +using System; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +using AdvancedSystems.Security.Abstractions; + +namespace AdvancedSystems.Security.Extensions; + +public static class HashFunctionExtensions +{ + /// + /// Converts the specified to its corresponding instance of + /// . + /// + /// + /// The specified hash algorithm. + /// + /// + /// An instance of . + /// + /// + /// Raised if the value of cannot be backed by a built-in implementation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HashAlgorithmName ToHashAlgorithmName(this HashFunction hashFunction) + { + return hashFunction switch + { + HashFunction.MD5 => HashAlgorithmName.MD5, + HashFunction.SHA1 => HashAlgorithmName.SHA1, + HashFunction.SHA256 => HashAlgorithmName.SHA256, + HashFunction.SHA384 => HashAlgorithmName.SHA384, + HashFunction.SHA512 => HashAlgorithmName.SHA512, + HashFunction.SHA3_256 => HashAlgorithmName.SHA3_256, + HashFunction.SHA3_384 => HashAlgorithmName.SHA3_384, + HashFunction.SHA3_512 => HashAlgorithmName.SHA3_512, + _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."), + }; + } + + /// + /// Gets the size, in bits, of the computed hash code. + /// + /// + /// The specified hash algorithm. + /// + /// + /// Raised if the value of cannot be backed by a built-in implementation. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetSize(this HashFunction hashFunction) + { + return hashFunction switch + { + HashFunction.MD5 => 128, + HashFunction.SHA1 => 160, + HashFunction.SHA256 or HashFunction.SHA3_256 => 256, + HashFunction.SHA384 or HashFunction.SHA3_384 => 384, + HashFunction.SHA512 or HashFunction.SHA3_512 => 512, + _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."), + }; + } + + /// + /// Gets the name of the hash function. + /// + /// + /// The specified hash algorithm. + /// + /// + /// The name of the hash function. + /// + /// + /// Raised if the value of cannot be backed by a built-in implementation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetName(this HashFunction hashFunction) + { + return hashFunction switch + { + HashFunction.MD5 => "MD5", + HashFunction.SHA1 => "SHA1", + HashFunction.SHA256 => "SHA256", + HashFunction.SHA384 => "SHA384", + HashFunction.SHA512 => "SHA512", + HashFunction.SHA3_256 => "SHA3-256", + HashFunction.SHA3_384 => "SHA3-384", + HashFunction.SHA3_512 => "SHA3-512", + _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented.") + }; + } +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Services/CertificateService.cs b/AdvancedSystems.Security/Services/CertificateService.cs index 9deac68..0f288be 100644 --- a/AdvancedSystems.Security/Services/CertificateService.cs +++ b/AdvancedSystems.Security/Services/CertificateService.cs @@ -13,7 +13,12 @@ namespace AdvancedSystems.Security.Services; -/// +/// +/// Defines a service for managing and retrieving X.509 certificates. +/// +/// +/// +/// public sealed class CertificateService : ICertificateService { private readonly ILogger _logger; diff --git a/AdvancedSystems.Security/Services/CertificateStore.cs b/AdvancedSystems.Security/Services/CertificateStore.cs index af4ba11..fab5ac7 100644 --- a/AdvancedSystems.Security/Services/CertificateStore.cs +++ b/AdvancedSystems.Security/Services/CertificateStore.cs @@ -5,7 +5,9 @@ namespace AdvancedSystems.Security.Services; -/// +/// +/// Represents a service for an X.509 store, which is a physical store where certificates are persisted and managed. +/// public sealed class CertificateStore : ICertificateStore { private readonly X509Store _store; diff --git a/AdvancedSystems.Security/Services/CryptoRandomService.cs b/AdvancedSystems.Security/Services/CryptoRandomService.cs index 737246b..bfc0e2f 100644 --- a/AdvancedSystems.Security/Services/CryptoRandomService.cs +++ b/AdvancedSystems.Security/Services/CryptoRandomService.cs @@ -5,7 +5,9 @@ namespace AdvancedSystems.Security.Services; -/// +/// +/// Represents a service for performing cryptographically secure numerical operations. +/// public sealed class CryptoRandomService : ICryptoRandomService { public CryptoRandomService() diff --git a/AdvancedSystems.Security/Services/HMACService.cs b/AdvancedSystems.Security/Services/HMACService.cs new file mode 100644 index 0000000..3d64712 --- /dev/null +++ b/AdvancedSystems.Security/Services/HMACService.cs @@ -0,0 +1,20 @@ +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Cryptography; + +namespace AdvancedSystems.Security.Services; + +/// +/// Represents a service designed for computing hash algorithms. +/// +public sealed class HMACService : IHMACService +{ + #region Methods + + /// + public byte[] Compute(byte[] buffer, byte[] key, HashFunction hashFunction) + { + return HMACProvider.Compute(buffer, key, hashFunction); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Services/HashService.cs b/AdvancedSystems.Security/Services/HashService.cs index e89ac3a..4023017 100644 --- a/AdvancedSystems.Security/Services/HashService.cs +++ b/AdvancedSystems.Security/Services/HashService.cs @@ -1,7 +1,6 @@ -using System.Security.Cryptography; +using System.Diagnostics.CodeAnalysis; using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.Common; using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Extensions; @@ -9,7 +8,9 @@ namespace AdvancedSystems.Security.Services; -/// +/// +/// Represents a service designed for computing hash algorithms. +/// public sealed class HashService : IHashService { private readonly ILogger _logger; @@ -22,78 +23,23 @@ public HashService(ILogger logger) #region Methods /// - public string GetMD5Hash(byte[] buffer) + public byte[] Compute(byte[] buffer, HashFunction hashFunction) { - this._logger.LogWarning("Computing hash with a cryptographically insecure hash algorithm (MD5)."); + if (hashFunction is HashFunction.MD5 or HashFunction.SHA1) + { + this._logger.LogWarning( + "Computing hash with a cryptographically insecure hash algorithm ({HashFunction}).", + hashFunction.GetName() + ); + } - byte[] md5 = Hash.Compute(buffer, HashAlgorithmName.MD5); - return md5.ToString(Format.Hex); + return HashProvider.Compute(buffer, hashFunction); } /// - public string GetSHA1Hash(byte[] buffer) + public bool TryComputePBKDF2(byte[] password, byte[] salt, int hashSize, int iterations, HashFunction hashFunction, [NotNullWhen(true)] out byte[] pbkdf2) { - this._logger.LogWarning("Computing hash with a cryptographically insecure hash algorithm (SHA1)."); - - byte[] sha1 = Hash.Compute(buffer, HashAlgorithmName.SHA1); - return sha1.ToString(Format.Hex); - } - - /// - public string GetSHA256Hash(byte[] buffer) - { - byte[] sha256 = Hash.Compute(buffer, HashAlgorithmName.SHA256); - return sha256.ToString(Format.Hex); - } - - /// - public string GetSHA384Hash(byte[] buffer) - { - byte[] sha384 = Hash.Compute(buffer, HashAlgorithmName.SHA384); - return sha384.ToString(Format.Hex); - } - - /// - public string GetSHA512Hash(byte[] buffer) - { - byte[] sha512 = Hash.Compute(buffer, HashAlgorithmName.SHA512); - return sha512.ToString(Format.Hex); - } - - /// - public string GetSecureSHA1Hash(byte[] buffer, byte[] salt, int iterations = 1000) - { - var hashAlgorithmName = HashAlgorithmName.SHA1; - using var hashAlgorithm = Hash.Create(hashAlgorithmName); - bool isSuccessful = Hash.TryComputeSecure(buffer, salt, hashAlgorithm.HashSize, iterations, hashAlgorithmName, out byte[] sha1); - return isSuccessful ? sha1.ToString(Format.Hex) : string.Empty; - } - - /// - public string GetSecureSHA256Hash(byte[] buffer, byte[] salt, int iterations = 1000) - { - var hashAlgorithmName = HashAlgorithmName.SHA256; - using var hashAlgorithm = Hash.Create(hashAlgorithmName); - bool isSuccessful = Hash.TryComputeSecure(buffer, salt, hashAlgorithm.HashSize, iterations, hashAlgorithmName, out byte[] sha256); - return isSuccessful ? sha256.ToString(Format.Hex) : string.Empty; - } - - /// - public string GetSecureSHA384Hash(byte[] buffer, byte[] salt, int iterations = 1000) - { - var hashAlgorithmName = HashAlgorithmName.SHA384; - using var hashAlgorithm = Hash.Create(hashAlgorithmName); - bool isSuccessful = Hash.TryComputeSecure(buffer, salt, hashAlgorithm.HashSize, iterations, hashAlgorithmName, out byte[] sha384); - return isSuccessful ? sha384.ToString(Format.Hex) : string.Empty; - } - - /// - public string GetSecureSHA512Hash(byte[] buffer, byte[] salt, int iterations = 1000) - { - var hashAlgorithmName = HashAlgorithmName.SHA512; - using var hashAlgorithm = Hash.Create(hashAlgorithmName); - bool isSuccessful = Hash.TryComputeSecure(buffer, salt, hashAlgorithm.HashSize, iterations, hashAlgorithmName, out byte[] sha512); - return isSuccessful ? sha512.ToString(Format.Hex) : string.Empty; + return HashProvider.TryComputePBKDF2(password, salt, hashSize, iterations, hashFunction, out pbkdf2); } #endregion diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs index 27ca043..2fb4368 100644 --- a/AdvancedSystems.Security/Services/RSACryptoService.cs +++ b/AdvancedSystems.Security/Services/RSACryptoService.cs @@ -12,8 +12,10 @@ namespace AdvancedSystems.Security.Services; -/// -public sealed class RSACryptoService : IRSACryptoService +/// +/// Represents a service for performing RSA-based asymmetric operations. +/// +public sealed class RSACryptoService { private readonly ILogger _logger; private readonly ICertificateService _certificateService; @@ -109,12 +111,6 @@ public void Dispose(bool disposing) this._disposed = true; } - /// - public bool IsValidMessage(string message, RSAEncryptionPadding? padding, Encoding? encoding = null) - { - return this._provider.IsValidMessage(message, padding, encoding); - } - /// public string Encrypt(string message, Encoding? encoding = null) { diff --git a/docs/docs/providers.md b/docs/docs/providers.md new file mode 100644 index 0000000..87499d8 --- /dev/null +++ b/docs/docs/providers.md @@ -0,0 +1,19 @@ +# Providers + +TODO + +## CryptoRandomProvider + +TODO + +## Hash + +## Generic Hash Algorithms + +## PBKDF2 Derived Keys + +TODO + +## RSACryptoProvider + +TODO diff --git a/docs/docs/services.md b/docs/docs/services.md new file mode 100644 index 0000000..1ee8e0e --- /dev/null +++ b/docs/docs/services.md @@ -0,0 +1,23 @@ +# Services + +TODO + +## CertificateService + +TODO + +## CertificateStore + +TODO + +## CryptoRandomService + +TODO + +## HashService + +TODO + +## RSACryptoService + +TODO diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml index 47aa192..ec5a0c6 100644 --- a/docs/docs/toc.yml +++ b/docs/docs/toc.yml @@ -2,5 +2,9 @@ href: introduction.md - name: Getting Started href: getting-started.md +- name: Providers + href: providers.md +- name: Services + href: services.md - name: Changelog href: changelog.md From 6497026093f279d4a028cf547bb19b3c64f5a2da Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 1 Feb 2025 21:56:27 +0100 Subject: [PATCH 28/41] Revamp implementation of HMACProvider, and update unit tests * change structure of extenion methods --- .../HashFunction.cs | 4 + .../IHMACService.cs | 50 ++++-- .../Cryptography/HMACTests.cs | 149 ++++++++---------- .../Extensions/CoreExtensionsTests.cs | 66 ++++++++ .../Fixtures/HMACFixture.cs | 18 +++ .../Services/HMACServiceTests.cs | 48 ++++++ .../Cryptography/HMACProvider.cs | 47 ++---- .../Cryptography/HashProvider.cs | 47 ++---- AdvancedSystems.Security/Extensions/Bytes.cs | 23 --- .../Extensions/CoreExtensions.cs | 35 ++++ .../Extensions/StringExtensions.cs | 12 ++ .../Services/HMACService.cs | 10 +- 12 files changed, 318 insertions(+), 191 deletions(-) create mode 100644 AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs create mode 100644 AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs create mode 100644 AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs delete mode 100644 AdvancedSystems.Security/Extensions/Bytes.cs create mode 100644 AdvancedSystems.Security/Extensions/CoreExtensions.cs create mode 100644 AdvancedSystems.Security/Extensions/StringExtensions.cs diff --git a/AdvancedSystems.Security.Abstractions/HashFunction.cs b/AdvancedSystems.Security.Abstractions/HashFunction.cs index cebd2af..89ce318 100644 --- a/AdvancedSystems.Security.Abstractions/HashFunction.cs +++ b/AdvancedSystems.Security.Abstractions/HashFunction.cs @@ -1,5 +1,9 @@ namespace AdvancedSystems.Security.Abstractions; +/// +/// Identifies a mathematical function that maps a string of arbitrary length +/// (up to a pre-determined maximum size) to a fixed-length string. +/// public enum HashFunction { MD5 = 0, diff --git a/AdvancedSystems.Security.Abstractions/IHMACService.cs b/AdvancedSystems.Security.Abstractions/IHMACService.cs index 67acdb1..55c481c 100644 --- a/AdvancedSystems.Security.Abstractions/IHMACService.cs +++ b/AdvancedSystems.Security.Abstractions/IHMACService.cs @@ -10,24 +10,56 @@ public interface IHMACService #region Methods /// - /// Computes the hash value for the specified byte array using the specified . + /// Computes the HMAC of using the specified . /// - /// - /// The input to compute the hash code for. + /// + /// The hash function to use. /// /// - /// The secret key for HMAC computation. + /// The HMAC key. This cryptographic key uniquely identifies one or more entities. /// - /// - /// The hash algorithm implementation to use. + /// + /// The data to HMAC. /// /// - /// The computed hash code. + /// The HMAC of the data. This cryptographic checksum is the result of passing through + /// through a message authentication algorithm. /// /// - /// Raised if the specified is not implemented. + /// Raised if an unsupported is passed to this method. /// - public byte[] Compute(byte[] buffer, byte[] key, HashFunction hashFunction); + /// + /// + /// Message Authentication Codes (MACs) are primarily used for message authentication. MACs based on + /// cryptographic hash functions are known as HMACs. Their purpose is to authenticate both the source + /// of a message as well as its integrity. A message authentication algorithm is called HMAC, while the + /// result of applying HMAC is called the MAC. + /// + /// + /// HMAC keys should be generated from a cryptographically strong random number generator. + /// You must securely store the HMAC and treat this information as a secret. + /// The value of the secret key should only be known to the message originator and the intended receiver(s). + /// The recommended key size is at least 32-bytes for full-entropy keys. + /// + /// + /// The hash function does not require collision-resistance as long as the hash function meets the very + /// weak requirement of being almost universal (c.f. ). + /// FIPS complieant hash algorithms are defined by FIPS PUB 180-4: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// See also: . + /// + /// + public byte[] Compute(HashFunction hashFunction, ReadOnlySpan key, ReadOnlySpan buffer); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs index 5a4afb5..5fd78c9 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs @@ -1,4 +1,5 @@ -using System.Text; +using System; +using System.Text; using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; @@ -13,107 +14,85 @@ namespace AdvancedSystems.Security.Tests.Cryptography; /// public sealed class HMACTests { - #region Tests - - [Theory] - [InlineData("Hello, World!", "006255bea760447cfb4a2a83f8dcd78e", Format.Hex)] - [InlineData("Hello, World!", "AGJVvqdgRHz7SiqD+NzXjg==", Format.Base64)] - [InlineData("The quick brown fox jumps over the lazy dog", "39bbfdd604b4ce30df059be91ff1cfdc", Format.Hex)] - [InlineData("The quick brown fox jumps over the lazy dog", "Obv91gS0zjDfBZvpH/HP3A==", Format.Base64)] - public void TestMD5HMAC(string input, string expectedHash, Format format) - { - // Arrange - Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); - byte[] key = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - - // Act - byte[] hash = HMACProvider.Compute(buffer, key, HashFunction.MD5); - string md5 = hash.ToString(format); - - // Assert - Assert.Equal(expectedHash, md5); - } - + #region Test + + /// + /// Tests that + /// computes the MAC value of with as cryptographic + /// algorithm and a hard-coded key correctly. + /// + /// + /// The hash function to use. + /// + /// + /// The data to HMAC. + /// + /// + /// The expected MAC value for . + /// [Theory] - [InlineData("Hello, World!", "74337880adf8df0e331ba348e8a78ff3fbae4504", Format.Hex)] - [InlineData("Hello, World!", "dDN4gK343w4zG6NI6KeP8/uuRQQ=", Format.Base64)] - [InlineData("The quick brown fox jumps over the lazy dog", "6e5535b20cfe26e18df61b465029f379320108ff", Format.Hex)] - [InlineData("The quick brown fox jumps over the lazy dog", "blU1sgz+JuGN9htGUCnzeTIBCP8=", Format.Base64)] - public void TestSHA1HMAC(string input, string expectedHash, Format format) + [InlineData(HashFunction.MD5, "Hello, World!", "c8972c594d22ce2b4f73e91538db5737")] + [InlineData(HashFunction.MD5, "The quick brown fox jumps over the lazy dog", "2e3f3742c21be88e64deb2127fe792d2")] + [InlineData(HashFunction.SHA1, "Hello, World!", "883a982dc2ae46d20f7f106c786a9241b60dc340")] + [InlineData(HashFunction.SHA1, "The quick brown fox jumps over the lazy dog", "198ea1ea04c435c1246b586a06d5cf11c3ffcda6")] + [InlineData(HashFunction.SHA256, "Hello, World!", "fcfaffa7fef86515c7beb6b62d779fa4ccf092f2e61c164376054271252821ff")] + [InlineData(HashFunction.SHA256, "The quick brown fox jumps over the lazy dog", "54cd5b827c0ec938fa072a29b177469c843317b095591dc846767aa338bac600")] + [InlineData(HashFunction.SHA384, "Hello, World!", "699f60b90c6dc0a32be14690a22c541bc3eba8d3215a782c28d66db3f0a3eb81cc0703c932bc181b02d190c9f9967e41")] + [InlineData(HashFunction.SHA384, "The quick brown fox jumps over the lazy dog", "bf8a22d3bd5cf88e0f41fa90eeb00eb908fccd925d55a7305f23e206358bb488fbef01039308e434c255e59f8e3badc3")] + [InlineData(HashFunction.SHA512, "Hello, World!", "851caed63934ad1c9a03aef23ba2b84f224bdff4f5148efc57d95f9ae80ca9db2e98bc4c709a529eb1b7234a1ac2e381d28e0eb1efa090bb19613f5c124b6d5b")] + [InlineData(HashFunction.SHA512, "The quick brown fox jumps over the lazy dog", "76af3588620ef6e2c244d5a360e080c0d649b6dd6b82ccd115eeefee8ff403bcee9aeb08618db9a2a94a9e80c7996bb2cb0c00f6e69de38ed8af2758ef39df0a")] + public void TestHMAC_Value(HashFunction hashFunction, string text, string expectedMac) { // Arrange - Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); - byte[] key = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + byte[] key = "secret".GetBytes(Format.String); + byte[] buffer = text.GetBytes(Format.String); // Act - byte[] hash = HMACProvider.Compute(buffer, key, HashFunction.SHA1); - string sha1 = hash.ToString(format); + byte[] actualMac = HMACProvider.Compute(hashFunction, key, buffer); // Assert - Assert.Equal(expectedHash, sha1); + Assert.Equal(expectedMac.GetBytes(Format.Hex), actualMac); } + /// + /// Tests that + /// the computed MAC equals the size of . + /// + /// + /// The hash function to use. + /// + /// + /// The data to HMAC. + /// [Theory] - [InlineData("Hello, World!", "ac453c5917826e9a73ea7681c28b2156727432ac68477b144236b35b8507436c", Format.Hex)] - //[InlineData("Hello, World!", "8WReCbppz6naBwoshVnJ0MqxoR3sUQjazW4UHQ2w=", Format.Base64)] - //[InlineData("The quick brown fox jumps over the lazy dog", "b1d5f845047ccaada44062c2ba3d4f9d24572e1fc", Format.Hex)] - //[InlineData("The quick brown fox jumps over the lazy dog", "Z39ZJXtPfbhRPKsdX4RQR8yq2kQGLCuj1PnSRXLh/", Format.Base64)] - public void TestSHA256HMAC(string input, string expectedHash, Format format) + [InlineData(HashFunction.MD5, "Hello, World!")] + [InlineData(HashFunction.MD5, "The quick brown fox jumps over the lazy dog")] + [InlineData(HashFunction.SHA1, "Hello, World!")] + [InlineData(HashFunction.SHA1, "The quick brown fox jumps over the lazy dog")] + [InlineData(HashFunction.SHA256, "Hello, World!")] + [InlineData(HashFunction.SHA256, "The quick brown fox jumps over the lazy dog")] + [InlineData(HashFunction.SHA384, "Hello, World!")] + [InlineData(HashFunction.SHA384, "The quick brown fox jumps over the lazy dog")] + [InlineData(HashFunction.SHA512, "Hello, World!")] + [InlineData(HashFunction.SHA512, "The quick brown fox jumps over the lazy dog")] + public void TestHMAC_Size(HashFunction hashFunction, string text) { // Arrange - Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); - byte[] key = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + int keySize = 32; + byte[] buffer = text.GetBytes(Format.String); + int expectedMacSize = hashFunction.GetSize(); // Act - byte[] hash = HMACProvider.Compute(buffer, key, HashFunction.SHA256); - string sha256 = hash.ToString(format); + ReadOnlySpan key = CryptoRandomProvider.GetBytes(keySize); + byte[] mac = HMACProvider.Compute(hashFunction, key, buffer); // Assert - Assert.Equal(expectedHash, sha256); + Assert.Multiple(() => + { + Assert.NotEmpty(mac); + Assert.Equal(expectedMacSize, mac.Length * 8); + }); } - //[Theory] - //[InlineData("Hello, World!", "88474536c661377c09e2f81dac464b1ad3e4caeb9", Format.Hex)] - //[InlineData("Hello, World!", "AGJVvqdgRHz7SiqD+NzXjg==", Format.Base64)] - //[InlineData("The quick brown fox jumps over the lazy dog", "39bbfdd604b4ce30df059be91ff1cfdc", Format.Hex)] - //[InlineData("The quick brown fox jumps over the lazy dog", "Obv91gS0zjDfBZvpH/HP3A==", Format.Base64)] - //public void TestSHA384HMAC(string input, string expectedHash, Format format) - //{ - // // Arrange - // Encoding encoding = Encoding.UTF8; - // byte[] buffer = encoding.GetBytes(input); - // byte[] key = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - - // // Act - // byte[] hash = HMACProvider.Compute(buffer, key, HashFunction.SHA384); - // string sha384 = hash.ToString(format); - - // // Assert - // Assert.Equal(expectedHash, sha384); - //} - - //[Theory] - //[InlineData("Hello, World!", "c752bac8d1bcb622dddfded281b3cf2acf2c988c8", Format.Hex)] - //[InlineData("Hello, World!", "AGJVvqdgRHz7SiqD+NzXjg==", Format.Base64)] - //[InlineData("The quick brown fox jumps over the lazy dog", "39bbfdd604b4ce30df059be91ff1cfdc", Format.Hex)] - //[InlineData("The quick brown fox jumps over the lazy dog", "Obv91gS0zjDfBZvpH/HP3A==", Format.Base64)] - //public void TestSHA512HMAC(string input, string expectedHash, Format format) - //{ - // // Arrange - // Encoding encoding = Encoding.UTF8; - // byte[] buffer = encoding.GetBytes(input); - // byte[] key = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - - // // Act - // byte[] hash = HMACProvider.Compute(buffer, key, HashFunction.SHA512); - // string sha512 = hash.ToString(format); - - // // Assert - // Assert.Equal(expectedHash, sha512); - //} - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs b/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs new file mode 100644 index 0000000..d3905f0 --- /dev/null +++ b/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs @@ -0,0 +1,66 @@ +using System.Text; + +using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; + +using Xunit; + +namespace AdvancedSystems.Security.Tests.Extensions; + +public sealed class CoreExtensionsTests +{ + #region Tests + + [Theory] + [InlineData("test")] + [InlineData("Hello, World!")] + [InlineData("The quick brown fox jumps over the lazy dog")] + public void TestStringFormatting(string input) + { + // Arrange + byte[] buffer = input.GetBytes(Format.String); + + // Act + string fromBytes = buffer.ToString(Format.String); + byte[] @string = fromBytes.GetBytes(Format.String); + + // Assert + Assert.Equal(buffer, @string); + } + + [Theory] + [InlineData(1)] + [InlineData(69)] + [InlineData(100)] + public void TestBase64Formatting(int size) + { + // Arrange + byte[] buffer = CryptoRandomProvider.GetBytes(size).ToArray(); + + // Act + string base64 = buffer.ToString(Format.Base64); + byte[] fromBytes = base64.GetBytes(Format.Base64); + + // Assert + Assert.Equal(buffer, fromBytes); + } + + [Theory] + [InlineData(1)] + [InlineData(69)] + [InlineData(100)] + public void TestHexFormatting(int size) + { + // Arrange + byte[] buffer = CryptoRandomProvider.GetBytes(size).ToArray(); + + // Act + string hexadecimal = buffer.ToString(Format.Hex); + byte[] fromBytes = hexadecimal.GetBytes(Format.Hex); + + // Assert + Assert.Equal(buffer, fromBytes); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs new file mode 100644 index 0000000..461f097 --- /dev/null +++ b/AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs @@ -0,0 +1,18 @@ +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Services; + +namespace AdvancedSystems.Security.Tests.Fixtures; + +public sealed class HMACFixture +{ + public HMACFixture() + { + this.HMACService = new HMACService(); + } + + #region Properties + + public IHMACService HMACService { get; private set; } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs new file mode 100644 index 0000000..7c773ca --- /dev/null +++ b/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs @@ -0,0 +1,48 @@ +using System; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; +using AdvancedSystems.Security.Tests.Fixtures; + +using Xunit; + +namespace AdvancedSystems.Security.Tests.Services; + +/// +/// Tests the public methods in . +/// +public sealed class HMACServiceTests : IClassFixture, IClassFixture +{ + private readonly IHMACService _sut; + private readonly ICryptoRandomService _cryptoRandomService; + + public HMACServiceTests(HMACFixture fixture, CryptoRandomFixture cryptoRandomFixture) + { + this._sut = fixture.HMACService; + this._cryptoRandomService = cryptoRandomFixture.CryptoRandomService; + } + + #region Tests + + /// + /// Tests that + /// returns a non-empty result. + /// + [Fact] + public void TestCompute() + { + // Arrange + var sha256 = HashFunction.SHA256; + Span key = this._cryptoRandomService.GetBytes(32); + byte[] data = "Hello, World".GetBytes(Format.String); + + // Act + byte[] mac = this._sut.Compute(sha256, key, data); + + // Assert + Assert.NotEmpty(mac); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Cryptography/HMACProvider.cs b/AdvancedSystems.Security/Cryptography/HMACProvider.cs index 934126b..9dc9a41 100644 --- a/AdvancedSystems.Security/Cryptography/HMACProvider.cs +++ b/AdvancedSystems.Security/Cryptography/HMACProvider.cs @@ -6,49 +6,24 @@ namespace AdvancedSystems.Security.Cryptography; /// -/// Represents a class designed for computing HashProvider-Based Message Authentication Codes (HMACProvider). +/// Represents a class designed for computing Hash-Based Message Authentication Codes (HMAC). /// public static class HMACProvider { - /// - public static byte[] Compute(byte[] buffer, byte[] key, HashFunction hashFunction) - { - using var hmac = HMACProvider.Create(key, hashFunction); - return hmac.ComputeHash(buffer); - } - - #region Helpers - - /// - /// Creates a new instance of that implements . - /// - /// - /// The secret key for HMACProvider computation. - /// - /// - /// The hash function to use. - /// - /// - /// A new instance of . - /// - /// - /// Raised if the specified is not implemented. - /// - private static KeyedHashAlgorithm Create(byte[] key, HashFunction hashFunction) + /// + public static byte[] Compute(HashFunction hashFunction, ReadOnlySpan key, ReadOnlySpan buffer) { return hashFunction switch { - HashFunction.MD5 => new HMACMD5(key), - HashFunction.SHA1 => new HMACSHA1(key), - HashFunction.SHA256 => new HMACSHA256(key), - HashFunction.SHA384 => new HMACSHA384(key), - HashFunction.SHA512 => new HMACSHA512(key), - HashFunction.SHA3_256 => new HMACSHA3_256(key), - HashFunction.SHA3_384 => new HMACSHA3_384(key), - HashFunction.SHA3_512 => new HMACSHA3_512(key), + HashFunction.MD5 => HMACMD5.HashData(key, buffer), + HashFunction.SHA1 => HMACSHA1.HashData(key, buffer), + HashFunction.SHA256 => HMACSHA256.HashData(key, buffer), + HashFunction.SHA384 => HMACSHA384.HashData(key, buffer), + HashFunction.SHA512 => HMACSHA512.HashData(key, buffer), + HashFunction.SHA3_256 => HMACSHA3_256.HashData(key, buffer), + HashFunction.SHA3_384 => HMACSHA3_384.HashData(key, buffer), + HashFunction.SHA3_512 => HMACSHA3_512.HashData(key, buffer), _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."), }; } - - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security/Cryptography/HashProvider.cs b/AdvancedSystems.Security/Cryptography/HashProvider.cs index 236e186..5ce8a58 100644 --- a/AdvancedSystems.Security/Cryptography/HashProvider.cs +++ b/AdvancedSystems.Security/Cryptography/HashProvider.cs @@ -16,7 +16,19 @@ public static class HashProvider /// public static byte[] Compute(byte[] buffer, HashFunction hashFunction) { - using var hashAlgorithm = HashProvider.Create(hashFunction); + using HashAlgorithm hashAlgorithm = hashFunction switch + { + HashFunction.MD5 => MD5.Create(), + HashFunction.SHA1 => SHA1.Create(), + HashFunction.SHA256 => SHA256.Create(), + HashFunction.SHA384 => SHA384.Create(), + HashFunction.SHA512 => SHA512.Create(), + HashFunction.SHA3_256 => SHA3_256.Create(), + HashFunction.SHA3_384 => SHA3_384.Create(), + HashFunction.SHA3_512 => SHA3_512.Create(), + _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."), + }; + return hashAlgorithm.ComputeHash(buffer); } @@ -34,37 +46,4 @@ public static bool TryComputePBKDF2(byte[] password, byte[] salt, int hashSize, return false; } } - - #region Helpers - - /// - /// Creates a new instance of that implements . - /// - /// - /// The hash function to use. - /// - /// - /// A new instance of . - /// - /// - /// Raised if the specified is not implemented. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static HashAlgorithm Create(HashFunction hashFunction) - { - return hashFunction switch - { - HashFunction.MD5 => MD5.Create(), - HashFunction.SHA1 => SHA1.Create(), - HashFunction.SHA256 => SHA256.Create(), - HashFunction.SHA384 => SHA384.Create(), - HashFunction.SHA512 => SHA512.Create(), - HashFunction.SHA3_256 => SHA3_256.Create(), - HashFunction.SHA3_384 => SHA3_384.Create(), - HashFunction.SHA3_512 => SHA3_512.Create(), - _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."), - }; - } - - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security/Extensions/Bytes.cs b/AdvancedSystems.Security/Extensions/Bytes.cs deleted file mode 100644 index 86804be..0000000 --- a/AdvancedSystems.Security/Extensions/Bytes.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Text; - -using AdvancedSystems.Security.Cryptography; - -namespace AdvancedSystems.Security.Extensions; - -/// -/// Implements extension methods for manipulating byte arrays. -/// -public static class Bytes -{ - public static string ToString(this byte[] array, Format format) - { - return format switch - { - Format.Hex => BitConverter.ToString(array).Replace("-", string.Empty).ToLower(), - Format.Base64 => Convert.ToBase64String(array), - Format.String => Encoding.UTF8.GetString(array), - _ => throw new NotSupportedException($"String formatting is not implemted for {format}.") - }; - } -} \ No newline at end of file diff --git a/AdvancedSystems.Security/Extensions/CoreExtensions.cs b/AdvancedSystems.Security/Extensions/CoreExtensions.cs new file mode 100644 index 0000000..86327d8 --- /dev/null +++ b/AdvancedSystems.Security/Extensions/CoreExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Text; + +using AdvancedSystems.Security.Cryptography; + +namespace AdvancedSystems.Security.Extensions; + +/// +/// Implements extension methods built-in types. See also: +/// . +/// +public static class CoreExtensions +{ + public static string ToString(this byte[] array, Format format) + { + return format switch + { + Format.Hex => Convert.ToHexString(array).ToLower(), + Format.Base64 => Convert.ToBase64String(array), + Format.String => Encoding.UTF8.GetString(array), + _ => throw new NotSupportedException($"Case {format} is not implemented.") + }; + } + + public static byte[] GetBytes(this string @string, Format format) + { + return format switch + { + Format.Hex => Convert.FromHexString(@string), + Format.Base64 => Convert.FromBase64String(@string), + Format.String => Encoding.UTF8.GetBytes(@string), + _ => throw new NotSupportedException($"Case {format} is not implemented.") + }; + } +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Extensions/StringExtensions.cs b/AdvancedSystems.Security/Extensions/StringExtensions.cs new file mode 100644 index 0000000..1aba33b --- /dev/null +++ b/AdvancedSystems.Security/Extensions/StringExtensions.cs @@ -0,0 +1,12 @@ +using System; +using System.Runtime.CompilerServices; +using System.Text; + +using AdvancedSystems.Security.Cryptography; + +namespace AdvancedSystems.Security.Extensions; + +public static class StringExtensions +{ + +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Services/HMACService.cs b/AdvancedSystems.Security/Services/HMACService.cs index 3d64712..b371d4c 100644 --- a/AdvancedSystems.Security/Services/HMACService.cs +++ b/AdvancedSystems.Security/Services/HMACService.cs @@ -1,4 +1,6 @@ -using AdvancedSystems.Security.Abstractions; +using System; + +using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; namespace AdvancedSystems.Security.Services; @@ -10,10 +12,10 @@ public sealed class HMACService : IHMACService { #region Methods - /// - public byte[] Compute(byte[] buffer, byte[] key, HashFunction hashFunction) + /// + public byte[] Compute(HashFunction hashFunction, ReadOnlySpan key, ReadOnlySpan buffer) { - return HMACProvider.Compute(buffer, key, hashFunction); + return HMACProvider.Compute(hashFunction, key, buffer); } #endregion From 9bf920f88191bf93292f201dc9ac719a675e2945 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 1 Feb 2025 22:20:23 +0100 Subject: [PATCH 29/41] Fix off-by-one error --- AdvancedSystems.Security.Tests/Cryptography/HashTests.cs | 1 - AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs index 26b0d3a..21f80d7 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs @@ -239,7 +239,6 @@ public void TestTryComputePBKDF2_InvalidHashFunctions(HashFunction hashFunction, Assert.Multiple(() => { Assert.False(isSuccessful); - Assert.Equal(saltSize, salt.Length); Assert.Empty(hash); }); } diff --git a/AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs b/AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs index 2631b9b..c300d0f 100644 --- a/AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs +++ b/AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs @@ -42,7 +42,7 @@ public static void Shuffle(Span values) /// public static T Choice(Span values) { - int index = CryptoRandomProvider.GetInt32(0, values.Length); + int index = CryptoRandomProvider.GetInt32(0, values.Length - 1); return values[index]; } } \ No newline at end of file From 8c04e55d3597bcfc0813d057d6e59f2a12ea6d6a Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 1 Feb 2025 22:33:29 +0100 Subject: [PATCH 30/41] Fix unit test for hash function support, accounting for different platforms --- .../Cryptography/HashTests.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs index 21f80d7..64ac4b8 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Security.Cryptography; +using System.Text; using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; @@ -219,11 +220,11 @@ public void TestTryComputePBKDF2(HashFunction hashFunction, int saltSize) /// The salt size to use. /// [Theory] - [InlineData(HashFunction.MD5, 128)] + //[InlineData(HashFunction.MD5, 128)] [InlineData(HashFunction.SHA3_256, 128)] [InlineData(HashFunction.SHA3_384, 128)] [InlineData(HashFunction.SHA3_512, 128)] - public void TestTryComputePBKDF2_InvalidHashFunctions(HashFunction hashFunction, int saltSize) + public void TestTryComputePBKDF2_HashFunctionSupport(HashFunction hashFunction, int saltSize) { // Arrange int iterations = 100_000; @@ -238,8 +239,9 @@ public void TestTryComputePBKDF2_InvalidHashFunctions(HashFunction hashFunction, // Assert Assert.Multiple(() => { - Assert.False(isSuccessful); - Assert.Empty(hash); + // All current platforms support HMAC-SHA3-256, 384, and 512 together, so we can simplify the check + // to just checking HMAC-SHA3-256 for the availability of 384 and 512, too. + Assert.Equal(HMACSHA3_256.IsSupported, isSuccessful); }); } From 037cb6aed14562fe08abca4a018900e15e5193d4 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 1 Feb 2025 22:49:08 +0100 Subject: [PATCH 31/41] Update documentation for IHashService --- .../IHashService.cs | 20 +++++++++++-------- .../Cryptography/HashTests.cs | 1 - 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/AdvancedSystems.Security.Abstractions/IHashService.cs b/AdvancedSystems.Security.Abstractions/IHashService.cs index 794d0a9..3888fa4 100644 --- a/AdvancedSystems.Security.Abstractions/IHashService.cs +++ b/AdvancedSystems.Security.Abstractions/IHashService.cs @@ -46,7 +46,16 @@ public interface IHashService /// The number of iterations for the operation. /// /// - /// The hash algorithm to use to derive the hash. Supported algorithms are: + /// The hash algorithm to use to derive the hash. + /// + /// + /// A byte array containing the created PBKDF2 derived hash. + /// + /// + /// if the operation succeeds; otherwise, . + /// + /// + /// Supported algorithms for the parameter are: /// /// /// @@ -61,13 +70,8 @@ public interface IHashService /// /// /// - /// - /// - /// A byte array containing the created PBKDF2 derived hash. - /// - /// - /// if the operation succeeds; otherwise, . - /// + /// Additionally, some platforms may support SHA3-equivalent hash functions. + /// bool TryComputePBKDF2(byte[] password, byte[] salt, int hashSize, int iterations, HashFunction hashFunction, out byte[] pbkdf2); #endregion diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs index 64ac4b8..5ea2da0 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs @@ -220,7 +220,6 @@ public void TestTryComputePBKDF2(HashFunction hashFunction, int saltSize) /// The salt size to use. /// [Theory] - //[InlineData(HashFunction.MD5, 128)] [InlineData(HashFunction.SHA3_256, 128)] [InlineData(HashFunction.SHA3_384, 128)] [InlineData(HashFunction.SHA3_512, 128)] From 073a76ecce18169d4ce2cb8ff787dfc75e266978 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 1 Feb 2025 23:04:32 +0100 Subject: [PATCH 32/41] Change signature: HashFunction as first parameter --- .../IHashService.cs | 16 +++++----- .../Cryptography/HashTests.cs | 18 ++++++------ .../ServiceCollectionExtensionsTests.cs | 2 +- .../Services/HashServiceTests.cs | 4 +-- .../Cryptography/HashProvider.cs | 29 +++++++++---------- .../Services/HashService.cs | 8 ++--- 6 files changed, 37 insertions(+), 40 deletions(-) diff --git a/AdvancedSystems.Security.Abstractions/IHashService.cs b/AdvancedSystems.Security.Abstractions/IHashService.cs index 3888fa4..38c4f79 100644 --- a/AdvancedSystems.Security.Abstractions/IHashService.cs +++ b/AdvancedSystems.Security.Abstractions/IHashService.cs @@ -12,12 +12,12 @@ public interface IHashService /// /// Computes the hash value for the specified byte array. /// - /// - /// The input to compute the hash code for. - /// /// /// The hash algorithm implementation to use. /// + /// + /// The input to compute the hash code for. + /// /// /// The computed hash code. /// @@ -27,12 +27,15 @@ public interface IHashService /// /// Raised if the specified is not implemented. /// - byte[] Compute(byte[] buffer, HashFunction hashFunction); + byte[] Compute(HashFunction hashFunction, byte[] buffer); /// /// Attempts to compute a PBKDF2 (password-based key derivation function). /// This method is suitable for securely hashing passwords. /// + /// + /// The hash algorithm to use to derive the hash. + /// /// /// The password used to derive the hash. /// @@ -45,9 +48,6 @@ public interface IHashService /// /// The number of iterations for the operation. /// - /// - /// The hash algorithm to use to derive the hash. - /// /// /// A byte array containing the created PBKDF2 derived hash. /// @@ -72,7 +72,7 @@ public interface IHashService /// /// Additionally, some platforms may support SHA3-equivalent hash functions. /// - bool TryComputePBKDF2(byte[] password, byte[] salt, int hashSize, int iterations, HashFunction hashFunction, out byte[] pbkdf2); + bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, out byte[] pbkdf2); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs index 5ea2da0..779a979 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs @@ -40,7 +40,7 @@ public void TestMD5Hash(string input, string expected, Format format) byte[] buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(buffer, HashFunction.MD5); + byte[] hash = HashProvider.Compute(HashFunction.MD5, buffer); string md5 = hash.ToString(format); // Assert @@ -71,7 +71,7 @@ public void TestSHA1Hash(string input, string expected, Format format) byte[] buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(buffer, HashFunction.SHA1); + byte[] hash = HashProvider.Compute(HashFunction.SHA1, buffer); string sha1 = hash.ToString(format); // Assert @@ -102,7 +102,7 @@ public void TestSHA256Hash(string input, string expected, Format format) byte[] buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(buffer, HashFunction.SHA256); + byte[] hash = HashProvider.Compute(HashFunction.SHA256, buffer); string sha256 = hash.ToString(format); // Assert @@ -133,7 +133,7 @@ public void TestSHA384Hash(string input, string expected, Format format) byte[] buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(buffer, HashFunction.SHA384); + byte[] hash = HashProvider.Compute(HashFunction.SHA384, buffer); string sha384 = hash.ToString(format); // Assert @@ -164,7 +164,7 @@ public void TestSHA512Hash(string input, string expected, Format format) byte[] buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(buffer, HashFunction.SHA512); + byte[] hash = HashProvider.Compute(HashFunction.SHA512, buffer); string sha512 = hash.ToString(format); // Assert @@ -172,7 +172,7 @@ public void TestSHA512Hash(string input, string expected, Format format) } /// - /// Tests that + /// Tests that /// computes the hash code successfully and returns the hash with the expected size using the /// algorithm. /// @@ -197,7 +197,7 @@ public void TestTryComputePBKDF2(HashFunction hashFunction, int saltSize) byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); // Act - bool isSuccessful = HashProvider.TryComputePBKDF2(password, salt, hashSize, iterations, hashFunction, out byte[] hash); + bool isSuccessful = HashProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out byte[] hash); // Assert Assert.Multiple(() => @@ -209,7 +209,7 @@ public void TestTryComputePBKDF2(HashFunction hashFunction, int saltSize) } /// - /// Tests that + /// Tests that /// fails to compute the hash code on unsupported values and that the resulting /// hash is empty. /// @@ -233,7 +233,7 @@ public void TestTryComputePBKDF2_HashFunctionSupport(HashFunction hashFunction, byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); // Act - bool isSuccessful = HashProvider.TryComputePBKDF2(password, salt, hashSize, iterations, hashFunction, out byte[] hash); + bool isSuccessful = HashProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out byte[] hash); // Assert Assert.Multiple(() => diff --git a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index d2ab84b..d42d9eb 100644 --- a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -83,7 +83,7 @@ public async Task TestAddHashService() // Act var hashService = hostBuilder.Services.GetService(); - byte[]? sha256 = hashService?.Compute(buffer, HashFunction.SHA256); + byte[]? sha256 = hashService?.Compute(HashFunction.SHA256, buffer); // Assert Assert.Multiple(() => diff --git a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs index 62c3694..3984d09 100644 --- a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs @@ -32,7 +32,7 @@ public HashServiceTests(HashServiceFixture fixture) #region Tests /// - /// Tests that returns the expected hash, + /// Tests that returns the expected hash, /// and that the log warning message is called on or . /// /// @@ -51,7 +51,7 @@ public void TestCompute(string input, string expectedHash, HashFunction hashFunc this._sut.Logger.Invocations.Clear(); // Act - string actualHash = this._sut.HashService.Compute(buffer, hashFunction).ToString(Format.Hex); + string actualHash = this._sut.HashService.Compute(hashFunction, buffer).ToString(Format.Hex); // Assert Assert.Equal(expectedHash, actualHash); diff --git a/AdvancedSystems.Security/Cryptography/HashProvider.cs b/AdvancedSystems.Security/Cryptography/HashProvider.cs index 5ce8a58..4abecf5 100644 --- a/AdvancedSystems.Security/Cryptography/HashProvider.cs +++ b/AdvancedSystems.Security/Cryptography/HashProvider.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using System.Security.Cryptography; using AdvancedSystems.Security.Abstractions; @@ -13,27 +12,25 @@ namespace AdvancedSystems.Security.Cryptography; /// public static class HashProvider { - /// - public static byte[] Compute(byte[] buffer, HashFunction hashFunction) + /// + public static byte[] Compute(HashFunction hashFunction, byte[] buffer) { - using HashAlgorithm hashAlgorithm = hashFunction switch + return hashFunction switch { - HashFunction.MD5 => MD5.Create(), - HashFunction.SHA1 => SHA1.Create(), - HashFunction.SHA256 => SHA256.Create(), - HashFunction.SHA384 => SHA384.Create(), - HashFunction.SHA512 => SHA512.Create(), - HashFunction.SHA3_256 => SHA3_256.Create(), - HashFunction.SHA3_384 => SHA3_384.Create(), - HashFunction.SHA3_512 => SHA3_512.Create(), + HashFunction.MD5 => MD5.HashData(buffer), + HashFunction.SHA1 => SHA1.HashData(buffer), + HashFunction.SHA256 => SHA256.HashData(buffer), + HashFunction.SHA384 => SHA384.HashData(buffer), + HashFunction.SHA512 => SHA512.HashData(buffer), + HashFunction.SHA3_256 => SHA3_256.HashData(buffer), + HashFunction.SHA3_384 => SHA3_384.HashData(buffer), + HashFunction.SHA3_512 => SHA3_512.HashData(buffer), _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."), }; - - return hashAlgorithm.ComputeHash(buffer); } - /// - public static bool TryComputePBKDF2(byte[] password, byte[] salt, int hashSize, int iterations, HashFunction hashFunction, [NotNullWhen(true)] out byte[] pbkdf2) + /// + public static bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[] pbkdf2) { try { diff --git a/AdvancedSystems.Security/Services/HashService.cs b/AdvancedSystems.Security/Services/HashService.cs index 4023017..5acf167 100644 --- a/AdvancedSystems.Security/Services/HashService.cs +++ b/AdvancedSystems.Security/Services/HashService.cs @@ -23,7 +23,7 @@ public HashService(ILogger logger) #region Methods /// - public byte[] Compute(byte[] buffer, HashFunction hashFunction) + public byte[] Compute(HashFunction hashFunction, byte[] buffer) { if (hashFunction is HashFunction.MD5 or HashFunction.SHA1) { @@ -33,13 +33,13 @@ public byte[] Compute(byte[] buffer, HashFunction hashFunction) ); } - return HashProvider.Compute(buffer, hashFunction); + return HashProvider.Compute(hashFunction, buffer); } /// - public bool TryComputePBKDF2(byte[] password, byte[] salt, int hashSize, int iterations, HashFunction hashFunction, [NotNullWhen(true)] out byte[] pbkdf2) + public bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[] pbkdf2) { - return HashProvider.TryComputePBKDF2(password, salt, hashSize, iterations, hashFunction, out pbkdf2); + return HashProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out pbkdf2); } #endregion From e048aafd8b054ae25a1471950ec1e4348edb32bc Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 2 Feb 2025 18:14:49 +0100 Subject: [PATCH 33/41] Remove dead validator code --- .../IHMACService.cs | 1 + .../IHashService.cs | 3 +- .../Cryptography/HashProvider.cs | 4 +- .../Validators/CertificateOptionsValidator.cs | 61 ------------------- 4 files changed, 5 insertions(+), 64 deletions(-) delete mode 100644 AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs diff --git a/AdvancedSystems.Security.Abstractions/IHMACService.cs b/AdvancedSystems.Security.Abstractions/IHMACService.cs index 55c481c..ba5f8f0 100644 --- a/AdvancedSystems.Security.Abstractions/IHMACService.cs +++ b/AdvancedSystems.Security.Abstractions/IHMACService.cs @@ -5,6 +5,7 @@ namespace AdvancedSystems.Security.Abstractions; /// /// Represents a contract designed for computing Hash-Based Message Authentication Codes (HMAC). /// +/// public interface IHMACService { #region Methods diff --git a/AdvancedSystems.Security.Abstractions/IHashService.cs b/AdvancedSystems.Security.Abstractions/IHashService.cs index 38c4f79..a3b2ab7 100644 --- a/AdvancedSystems.Security.Abstractions/IHashService.cs +++ b/AdvancedSystems.Security.Abstractions/IHashService.cs @@ -5,6 +5,7 @@ namespace AdvancedSystems.Security.Abstractions; /// /// Represents a contract designed for computing hash algorithms. /// +/// public interface IHashService { #region Methods @@ -72,7 +73,7 @@ public interface IHashService /// /// Additionally, some platforms may support SHA3-equivalent hash functions. /// - bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, out byte[] pbkdf2); + bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, out byte[]? pbkdf2); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security/Cryptography/HashProvider.cs b/AdvancedSystems.Security/Cryptography/HashProvider.cs index 4abecf5..f378ddd 100644 --- a/AdvancedSystems.Security/Cryptography/HashProvider.cs +++ b/AdvancedSystems.Security/Cryptography/HashProvider.cs @@ -30,7 +30,7 @@ public static byte[] Compute(HashFunction hashFunction, byte[] buffer) } /// - public static bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[] pbkdf2) + public static bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2) { try { @@ -39,7 +39,7 @@ public static bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, } catch (Exception) { - pbkdf2 = []; + pbkdf2 = null; return false; } } diff --git a/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs b/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs deleted file mode 100644 index 2774206..0000000 --- a/AdvancedSystems.Security/Validators/CertificateOptionsValidator.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Security.Cryptography.X509Certificates; - -using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.Abstractions.Exceptions; -using AdvancedSystems.Security.Options; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace AdvancedSystems.Security.Validators; - -//public sealed class CertificateOptionsValidator : IValidateOptions -//{ -// private readonly ILogger _logger; -// private readonly ICertificateService _certificateService; - -// public CertificateOptionsValidator(ILogger logger, ICertificateService certificateService) -// { -// this._logger = logger; -// this._certificateService = certificateService; -// } - -// #region Implementation - -// public ValidateOptionsResult Validate(string? name, CertificateOptions options) -// { -// this._logger.LogDebug("Started validation of {Options}", nameof(CertificateOptions)); - -// if (string.IsNullOrEmpty(options.Thumbprint)) -// { -// return ValidateOptionsResult.Fail("Thumbprint is null or empty."); -// } - -// try -// { -// CertificateStoreOptions? store = options.Store; - -// if (store == null) -// { -// return ValidateOptionsResult.Fail("Certificate store is not configured."); -// } - -// X509Certificate2? certificate = this._certificateService.GetCertificate("default", options.Thumbprint, validOnly: false); - -// if (certificate is null) -// { -// return ValidateOptionsResult.Fail($"Configured certificate with thumbprint \"{options.Thumbprint}\" could not be found."); -// } -// } -// catch (CertificateNotFoundException exception) -// { -// return ValidateOptionsResult.Fail(exception.Message); -// } - -// this._logger.LogDebug("Completed validation of {Options}", nameof(CertificateOptions)); - -// return ValidateOptionsResult.Success; -// } - -// #endregion -//} \ No newline at end of file From dd263f68d0f0e9272df7ffcc689388ef1e0a5814 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 2 Feb 2025 22:07:53 +0100 Subject: [PATCH 34/41] Rename password certificate file and add encrypted private key to test assets --- .github/workflows/dotnet-tests.yml | 6 ++-- ...> AdvancedSystems-PasswordCertificate.pem} | 0 development/AdvancedSystems-PrivateKey.pk8 | 30 +++++++++++++++++++ development/about.md | 14 ++++++--- 4 files changed, 43 insertions(+), 7 deletions(-) rename development/{AdvancedSystems-Password.pem => AdvancedSystems-PasswordCertificate.pem} (100%) create mode 100644 development/AdvancedSystems-PrivateKey.pk8 diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml index ef8963e..fe16a76 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/dotnet-tests.yml @@ -51,7 +51,7 @@ jobs: $AppSettings = Get-Content '.\AdvancedSystems.Security.Tests\appsettings.json' -Raw | ConvertFrom-Json $Name = $AppSettings.CertificateStore.Name $Location = $AppSettings.CertificateStore.Location - Import-Certificate -FilePath .\development\AdvancedSystems-Password.pem -CertStoreLocation "Cert:\$Location\$Name" + Import-Certificate -FilePath .\development\AdvancedSystems-PasswordCertificate.pem -CertStoreLocation "Cert:\$Location\$Name" shell: powershell - name: Import Password Certificate (Ubuntu) @@ -60,7 +60,7 @@ jobs: appSettings='./AdvancedSystems.Security.Tests/appsettings.json' name=$(jq -r '.CertificateStore.Name' $appSettings) location=$(jq -r '.CertificateStore.Location' $appSettings) - dotnet certificate-tool add --file ./development/AdvancedSystems-Password.pem --store-name $name --store-location $location + dotnet certificate-tool add --file ./development/AdvancedSystems-PasswordCertificate.pem --store-name $name --store-location $location - name: Import Password Certificate (MacOS) if: runner.os == 'macOS' @@ -68,7 +68,7 @@ jobs: appSettings='./AdvancedSystems.Security.Tests/appsettings.json' name=$(jq -r '.CertificateStore.Name' $appSettings) location=$(jq -r '.CertificateStore.Location' $appSettings) - dotnet certificate-tool add --file ./development/AdvancedSystems-Password.pem --store-name $name --store-location $location + dotnet certificate-tool add --file ./development/AdvancedSystems-PasswordCertificate.pem --store-name $name --store-location $location - name: Configure DotNet User Secrets run: | diff --git a/development/AdvancedSystems-Password.pem b/development/AdvancedSystems-PasswordCertificate.pem similarity index 100% rename from development/AdvancedSystems-Password.pem rename to development/AdvancedSystems-PasswordCertificate.pem diff --git a/development/AdvancedSystems-PrivateKey.pk8 b/development/AdvancedSystems-PrivateKey.pk8 new file mode 100644 index 0000000..fdcab7a --- /dev/null +++ b/development/AdvancedSystems-PrivateKey.pk8 @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQkQCzXkHN/Q7fs2Tj +4Udh+QICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAAhhM+x1nzLWsTx +iGot3jMEggTQ2rYdwJqpw74JqQOM40wXh84Jy6B8974mgm1s74A355BQJWtSJGiF +/MjsJptpVC8yaLIw4EY/D9DniFw37hzeJHDnojwm4QEwR99ZhRyWsnY46akA7DjV +9U/OIcXTE3oLPnPc2wHHmVGu3GH+GvojaZ4UpCBqICF/KW+FP16BmFOI+7PLrdck +8ANihBLtPRcEDN7GF7Bj20Oxu24EO9qlQOl8XYqN6uZB6qLubR+TnuWypYe7ah/Q +Fe0uTxv7O8pnplsrKoJQt6/WlCA3lkJcacWwzXa3pnSCir4GeBgCoJyl27raCuiQ +OvSGVXO4vnu5hdYIMjZv0PF/WLBPRgXNjVyLCOQ00HQf7RMSrwaen/U2Mv9rg4S9 ++LW5jc+HD2ghZkSdFXHzbu+lHOApx3lA4i+tXIepxC82lIH+5wPpJ6I2qu6EQLr5 +hImF8vcjRmkLm0KvR5Py0tT+7SvwINJQxFxJwkxfYp2ewCeiLVl7GJvvkMhZZLDj +DhRuMH9fFtsaFlG5350gEDzMeKrFnvZMLSem+9vfZRTB63g8h5DPpmGK8jTaO4d9 +ebNv770cKHaQs+D2j5x109rFrQVBugNXbIH7JsKnvqJat2roezJOhyzDOVyM+8G8 +r5BrZ565MehrdRX31aFTKWvBhT/hrUkpH4TBAhAoTF++WVZDQdCECWEOZbQyRl3P +LxzXoHKh8ilgbrpjUF+9tDogWC6MhTqFHcb2hbzdO5A1hIyY0o8avXvltkQe6G5r +PnBGrqKxWfctVFo/7zkfmQFOwMa1vYyWHXE5va6StwPRDBrWy24ayh2UwI2YRTDQ +1y8+y0edhrbquCYg9x6b4zQpNcu9YRUFv/EfRQoEUWcGj9JzZAI07G2CTIySee5l +I4nwQbvFILTGNJ3HQVJ11AJwaLu/4B453T/TQRzmR/FI4/RgSF++Nz1VXq7cnXz6 +2N96pXE7j03HPszkoqjLs0KWUGHKLIuON9L3K66zSTiNF69TZzrdVevsj559C4eL +FX/wIpC6TwKAozAaKN6g6NyDb24pNBMAL9d0j/D6o65dPDSOrNGwjr60QNXjvf0M +1UaU5DP4+VQKXJIAkxpL32eD4ayrpqxblAoVQs/vwN3JWPwgTjodznt2D7hdDQvo +mzzyvMU8XWRkGKxKjNNtyiNd6gzfoYIcMxFiCTJGuIWo49tzWE/7uRqGfX9lMf+D +vDCeomH8zJNT2Rfqe7e1j0QG7u84+JYu91skUJ9Z0XKkMyWPDHD/75NlM+pFZcMC +CmTsqxO729fPuI/iyRKAInyGC7uGWN2yWd+rrggZ4nNSm90kZpOyuyCI7BfLC7jg +zXym+PES7c68uw96dQNwjJG8RsoRK17CpXN4/CXnuXlN5bvmMBVcssrNKaMWbEA7 +18SqDzxEHWw0LnWv4Pi3QrRVcZtBUqzL06vMp6t7cqa+MRCLzQXIrk89RNVfk7Cu +zcOuBnI5VresUtplv9x+UdhzWtkpZP3ch3h/bUqVN5dPEDX7raKAwyHf2FUqlsQc +GzjPjzWCv1AAQQqslyw8crl07t45WcjyVPUGrzavzhP+G5xoDGAKBx2mfKMVwHON +TVDUcJbMl1JBBj3WhFbNTDy+maYDFozGp4dQK+0CA+drbQC2I9c7PeI= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/development/about.md b/development/about.md index 8b196b4..358e3bb 100644 --- a/development/about.md +++ b/development/about.md @@ -5,7 +5,13 @@ purposes only. ## Certificates -| Name | Thumbprint | -|------------------------------|--------------------------------------------| -| AdvancedSystems-CA.pfx | `2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC` | -| AdvancedSystems-Password.pem | `F0078AAD21DECAC0BB5FB6400ABB4198F98441A8` | +| Name | Thumbprint | +|-----------------------------------------|--------------------------------------------| +| AdvancedSystems-CA.pfx | `2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC` | +| AdvancedSystems-PasswordCertificate.pem | `F0078AAD21DECAC0BB5FB6400ABB4198F98441A8` | + +## Keys + +| Name | Originates From | +|-----------------------------------------|--------------------------------------------| +| AdvancedSystems-PrivateKey.pk8 | AdvancedSystems-PasswordCertificate.pem | From c04ff8f83d93d058e6d7758857fbf6bebea8794c Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 2 Feb 2025 22:10:28 +0100 Subject: [PATCH 35/41] Fix implementation of TryImportPemCertificate and implement missing unit test --- .../ICertificateService.cs | 26 ++-- .../Cryptography/HashTests.cs | 6 +- .../Helpers/Assets.cs | 18 +++ .../Services/CertificateServiceTests.cs | 39 +++-- .../Services/CertificateService.cs | 139 +++++------------- .../Services/HashService.cs | 2 +- 6 files changed, 98 insertions(+), 132 deletions(-) create mode 100644 AdvancedSystems.Security.Tests/Helpers/Assets.cs diff --git a/AdvancedSystems.Security.Abstractions/ICertificateService.cs b/AdvancedSystems.Security.Abstractions/ICertificateService.cs index d122942..84954d4 100644 --- a/AdvancedSystems.Security.Abstractions/ICertificateService.cs +++ b/AdvancedSystems.Security.Abstractions/ICertificateService.cs @@ -35,8 +35,8 @@ public interface ICertificateService /// /// /// - /// - /// + /// + /// /// /// /// @@ -50,7 +50,7 @@ public interface ICertificateService /// /// /// - bool TryImportPemCertificate(string storeService, string publicKeyPath, string privateKeyPath, out X509Certificate2? certificate); + bool TryImportPemCertificate(string storeService, string certificatePath, string privateKeyPath, out X509Certificate2? certificate); /// /// Tries to import a PEM certificate file into a certificate store. @@ -58,14 +58,14 @@ public interface ICertificateService /// /// The name of the keyed service to use. /// - /// - /// The file path to the PEM file containing the public key or certificate. + /// + /// The file path to the PEM certificate. /// /// - /// The file path to the PEM file containing the private key associated with the public key. + /// The file path to the PKCS#8 (encrypted) private key associated with the specified certificate. /// /// - /// The password required to decrypt the private key in the PEM file. + /// The password required to decrypt the private key (if specified). /// /// /// An output parameter that will contain the imported instance if the operation succeeds; @@ -77,7 +77,7 @@ public interface ICertificateService /// /// See also: . /// - bool TryImportPemCertificate(string storeService, string publicKeyPath, string privateKeyPath, string password, out X509Certificate2? certificate); + bool TryImportPemCertificate(string storeService, string certificatePath, string privateKeyPath, string password, out X509Certificate2? certificate); /// /// @@ -85,8 +85,8 @@ public interface ICertificateService /// /// /// - /// - /// + /// + /// /// /// /// @@ -97,7 +97,7 @@ public interface ICertificateService /// /// /// - bool TryImportPfxCertificate(string storeService, string path, out X509Certificate2? certificate); + bool TryImportPfxCertificate(string storeService, string certificatePath, out X509Certificate2? certificate); /// /// Tries to import a PFX certificate file into a certificate store. @@ -105,7 +105,7 @@ public interface ICertificateService /// /// The name of the keyed service to use. /// - /// + /// /// The file path to the PFX certificate file that needs to be imported. /// /// @@ -121,7 +121,7 @@ public interface ICertificateService /// /// See also: . /// - bool TryImportPfxCertificate(string storeService, string path, string password, out X509Certificate2? certificate); + bool TryImportPfxCertificate(string storeService, string certificatePath, string password, out X509Certificate2? certificate); /// /// Retrieves all certificates from the certificate store. diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs index 779a979..3c916cc 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs @@ -197,14 +197,14 @@ public void TestTryComputePBKDF2(HashFunction hashFunction, int saltSize) byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); // Act - bool isSuccessful = HashProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out byte[] hash); + bool isSuccessful = HashProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out byte[]? hash); // Assert Assert.Multiple(() => { Assert.True(isSuccessful); Assert.Equal(saltSize, salt.Length); - Assert.Equal(hashSize, hash.Length); + Assert.Equal(hashSize, hash?.Length); }); } @@ -233,7 +233,7 @@ public void TestTryComputePBKDF2_HashFunctionSupport(HashFunction hashFunction, byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); // Act - bool isSuccessful = HashProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out byte[] hash); + bool isSuccessful = HashProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out byte[]? hash); // Assert Assert.Multiple(() => diff --git a/AdvancedSystems.Security.Tests/Helpers/Assets.cs b/AdvancedSystems.Security.Tests/Helpers/Assets.cs new file mode 100644 index 0000000..42685de --- /dev/null +++ b/AdvancedSystems.Security.Tests/Helpers/Assets.cs @@ -0,0 +1,18 @@ +using System; +using System.IO; + +namespace AdvancedSystems.Security.Tests.Helpers; + +internal static class Assets +{ + internal static string ProjectRoot + { + get + { + DirectoryInfo projectRoot = Directory.GetParent(Environment.CurrentDirectory)?.Parent?.Parent?.Parent + ?? throw new DirectoryNotFoundException("Failed to locate the project root directory."); + + return projectRoot.FullName; + } + } +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs index 001e917..e4ad01d 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs @@ -29,22 +29,36 @@ public CertificateServiceTests(CertificateFixture certificateFixture) #region Tests - [Fact(Skip = "TODO")] - public void TestTryImportPemCertificate_PKCS8_Header() + /// + /// Tests that + /// successfully imports a password-protected PEM certificate. + /// + [Fact] + public void TestTryImportPemCertificate_WithPassword() { + // Arrange + string storeService = this._sut.ConfiguredStoreService; + string publicKey = Path.Combine(Assets.ProjectRoot, "development", "AdvancedSystems-PasswordCertificate.pem"); + string privateKey = Path.Combine(Assets.ProjectRoot, "development", "AdvancedSystems-PrivateKey.pk8"); - } - - [Fact(Skip = "TODO")] - public void TestTryImportPemCertificate_Encrypted_PKCS8_Header() - { + var configuration = new ConfigurationBuilder() + .AddUserSecrets() + .Build(); - } + string? password = configuration[UserSecrets.CERTIFICATE_PASSWORD]; + Skip.If(string.IsNullOrEmpty(password), "A dotnet user-secrets is not configured for this test."); - [Fact(Skip = "TODO")] - public void TestTryImportPemCertificate_RSA_Header() - { + // Act + ICertificateService? certificateService = this._sut.Host?.Services.GetService(); + bool? isImported = certificateService?.TryImportPemCertificate(storeService, publicKey, privateKey, password, out _); + // Assert + Assert.Multiple(() => + { + Assert.NotNull(certificateService); + Assert.True(isImported.HasValue); + Assert.True(isImported.Value); + }); } /// @@ -56,7 +70,7 @@ public void TestTryImportPfxCertificate() { // Arrange string storeService = this._sut.ConfiguredStoreService; - string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "development", "AdvancedSystems-CA.pfx"); + string path = Path.Combine(Assets.ProjectRoot, "development", "AdvancedSystems-CA.pfx"); var configuration = new ConfigurationBuilder() .AddUserSecrets() @@ -74,6 +88,7 @@ public void TestTryImportPfxCertificate() { Assert.NotNull(certificateService); Assert.True(isImported.HasValue); + Assert.True(isImported.Value); }); } diff --git a/AdvancedSystems.Security/Services/CertificateService.cs b/AdvancedSystems.Security/Services/CertificateService.cs index 0f288be..0e2441c 100644 --- a/AdvancedSystems.Security/Services/CertificateService.cs +++ b/AdvancedSystems.Security/Services/CertificateService.cs @@ -61,101 +61,54 @@ public bool AddCertificate(string storeService, X509Certificate2 certificate) } /// - public bool TryImportPemCertificate(string storeService, string publicKeyPath, string? privateKeyPath, [NotNullWhen(true)] out X509Certificate2? certificate) + public bool TryImportPemCertificate(string storeService, string certificatePath, string? privateKeyPath, [NotNullWhen(true)] out X509Certificate2? certificate) { - return this.TryImportPemCertificate(storeService, publicKeyPath, privateKeyPath, string.Empty, out certificate); + return this.TryImportPemCertificate(storeService, certificatePath, privateKeyPath, string.Empty, out certificate); } /// - public bool TryImportPemCertificate(string storeService, string publicKeyPath, string? privateKeyPath, string password, [NotNullWhen(true)] out X509Certificate2? certificate) + public bool TryImportPemCertificate(string storeService, string certificatePath, string? privateKeyPath, string password, [NotNullWhen(true)] out X509Certificate2? certificate) { - if (!File.Exists(publicKeyPath)) - { - this._logger.LogError( - "Public key file does not exist (PublicKey=\"{PublicKey}\").", - publicKeyPath - ); - - certificate = null; - return false; - } - try { - using var publicKey = string.IsNullOrEmpty(password) - ? new X509Certificate2(publicKeyPath) - : new X509Certificate2(publicKeyPath, password, KeyStorageFlags); + bool withPassword = !string.IsNullOrEmpty(password); + + using var pemCertificate = withPassword + ? new X509Certificate2(certificatePath, password, KeyStorageFlags) + : new X509Certificate2(certificatePath); if (!string.IsNullOrEmpty(privateKeyPath)) { - if (!File.Exists(privateKeyPath)) - { - this._logger.LogError( - "Private key file does not exist (PrivateKey=\"{PrivateKey}\").", - privateKeyPath - ); - - certificate = null; - return false; - } - - string[] privateKeyBlocks = File.ReadAllText(privateKeyPath) - .Split("-", StringSplitOptions.RemoveEmptyEntries); - - string header = privateKeyBlocks[0]; - - byte[] privateKeyBuffer = Convert.FromBase64String(privateKeyBlocks[1]); + string pemContent = File.ReadAllText(privateKeyPath); using var privateKey = RSA.Create(); - switch (header) + if (withPassword) + { + privateKey.ImportFromEncryptedPem(pemContent, password); + } + else { - case PKCS8_PRIVATE_KEY_HEADER: - privateKey.ImportPkcs8PrivateKey(privateKeyBuffer, out _); - break; - case PKCS8_ENCRYPTED_PRIVATE_KEY_HEADER: - privateKey.ImportEncryptedPkcs8PrivateKey(password, privateKeyBuffer, out _); - break; - case RSA_PRIVATE_KEY_HEADER: - privateKey.ImportRSAPrivateKey(privateKeyBuffer, out _); - break; - default: - this._logger.LogCritical( - "Unknown header in private key: {Header} (\"{PrivateKey}\").", - header, - privateKeyPath - ); - - certificate = null; - return false; + privateKey.ImportFromPem(pemContent); } - certificate = publicKey.CopyWithPrivateKey(privateKey); + certificate = pemCertificate.CopyWithPrivateKey(privateKey); } else { - certificate = publicKey; + certificate = pemCertificate; } bool isImported = this.AddCertificate(storeService, certificate); return isImported; } - catch (CryptographicException) + catch (Exception exception) { - if (!string.IsNullOrEmpty(privateKeyPath)) - { - this._logger.LogError( - "Failed to initialize public key or private key from path (PublicKey=\"{PublicKey}\",PrivateKey=\"{PrivateKey}\").", - publicKeyPath, - privateKeyPath - ); - } - else - { - this._logger.LogError( - "Failed to initialize public key from path (PublicKey=\"{PublicKey}\").", - publicKeyPath - ); - } + this._logger.LogError( + "Failed to initialize public key or private key from path (PublicKey=\"{PublicKey}\",PrivateKey=\"{PrivateKey}\"): {Reason}", + certificatePath, + privateKeyPath ?? "unspecified", + exception.Message + ); certificate = null; return false; @@ -163,30 +116,21 @@ public bool TryImportPemCertificate(string storeService, string publicKeyPath, s } /// - public bool TryImportPfxCertificate(string storeService, string path, [NotNullWhen(true)] out X509Certificate2? certificate) + public bool TryImportPfxCertificate(string storeService, string certificatePath, [NotNullWhen(true)] out X509Certificate2? certificate) { - return this.TryImportPfxCertificate(storeService, path, string.Empty, out certificate); + return this.TryImportPfxCertificate(storeService, certificatePath, string.Empty, out certificate); } /// - public bool TryImportPfxCertificate(string storeService, string path, string password, [NotNullWhen(true)] out X509Certificate2? certificate) + public bool TryImportPfxCertificate(string storeService, string certificatePath, string password, [NotNullWhen(true)] out X509Certificate2? certificate) { - if (!File.Exists(path)) - { - this._logger.LogError( - "Certificate file does not exist (Path=\"{Certificate}\").", - path - ); - - certificate = null; - return false; - } - try { - certificate = string.IsNullOrEmpty(password) - ? new X509Certificate2(path) - : new X509Certificate2(path, password, KeyStorageFlags); + bool withPassword = !string.IsNullOrEmpty(password); + + certificate = withPassword + ? new X509Certificate2(certificatePath, password, KeyStorageFlags) + : new X509Certificate2(certificatePath); bool isImported = this.AddCertificate(storeService, certificate); return isImported; @@ -195,7 +139,7 @@ public bool TryImportPfxCertificate(string storeService, string path, string pas { this._logger.LogError( "Failed to initialize certificate from path (Path=\"{Certificate}\").", - path + certificatePath ); certificate = null; @@ -284,24 +228,13 @@ public bool RemoveCertificate(string storeService, string thumbprint) #region Helpers - private const string PKCS8_PRIVATE_KEY_HEADER = "BEGIN PRIVATE KEY"; - - private const string PKCS8_ENCRYPTED_PRIVATE_KEY_HEADER = "BEGIN ENCRYPTED PRIVATE KEY"; - - private const string RSA_PRIVATE_KEY_HEADER = "BEGIN RSA PRIVATE KEY"; - private static X509KeyStorageFlags KeyStorageFlags { get { - var keyStorageFlags = X509KeyStorageFlags.DefaultKeySet; - - if (OperatingSystem.IsMacOS()) - { - keyStorageFlags = X509KeyStorageFlags.Exportable; - } - - return keyStorageFlags; + return OperatingSystem.IsMacOS() + ? X509KeyStorageFlags.Exportable + : X509KeyStorageFlags.DefaultKeySet; } } diff --git a/AdvancedSystems.Security/Services/HashService.cs b/AdvancedSystems.Security/Services/HashService.cs index 5acf167..9b6f34f 100644 --- a/AdvancedSystems.Security/Services/HashService.cs +++ b/AdvancedSystems.Security/Services/HashService.cs @@ -37,7 +37,7 @@ public byte[] Compute(HashFunction hashFunction, byte[] buffer) } /// - public bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[] pbkdf2) + public bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2) { return HashProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out pbkdf2); } From 32addf2c5debdc228167b6d10af3f112f724b17d Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 2 Feb 2025 22:21:28 +0100 Subject: [PATCH 36/41] Don't log full paths of certificates or keys in log --- .../Services/CertificateService.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/AdvancedSystems.Security/Services/CertificateService.cs b/AdvancedSystems.Security/Services/CertificateService.cs index 0e2441c..e954cfb 100644 --- a/AdvancedSystems.Security/Services/CertificateService.cs +++ b/AdvancedSystems.Security/Services/CertificateService.cs @@ -104,9 +104,7 @@ public bool TryImportPemCertificate(string storeService, string certificatePath, catch (Exception exception) { this._logger.LogError( - "Failed to initialize public key or private key from path (PublicKey=\"{PublicKey}\",PrivateKey=\"{PrivateKey}\"): {Reason}", - certificatePath, - privateKeyPath ?? "unspecified", + "Failed to initialize public key or private key from path: {Reason}.", exception.Message ); @@ -135,11 +133,11 @@ public bool TryImportPfxCertificate(string storeService, string certificatePath, bool isImported = this.AddCertificate(storeService, certificate); return isImported; } - catch (CryptographicException) + catch (CryptographicException exception) { this._logger.LogError( - "Failed to initialize certificate from path (Path=\"{Certificate}\").", - certificatePath + "Failed to initialize certificate from path: {Reason}.", + exception.Message ); certificate = null; From 4c1ab477e6f950ef962078b3266e2ba2f8dd8301 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 5 Feb 2025 19:16:29 +0100 Subject: [PATCH 37/41] Implement KDFProvider, KDFService, IKDFService and add unit tests. --- .../IHashService.cs | 45 ---- .../IKDFService.cs | 57 +++++ .../Cryptography/HashTests.cs | 73 ------ .../Cryptography/KDFProviderTests.cs | 91 +++++++ .../ServiceCollectionExtensionsTests.cs | 229 +++++++++--------- .../Fixtures/CertificateFixture.cs | 2 +- .../Fixtures/CertificateStoreFixture.cs | 2 +- .../Fixtures/CryptoRandomFixture.cs | 2 +- .../Fixtures/HashFixture.cs | 2 +- .../Fixtures/KDFServiceFixture.cs | 18 ++ .../Services/KDFServiceTests.cs | 50 ++++ .../Cryptography/HashProvider.cs | 17 -- .../Cryptography/KDFProvider.cs | 29 +++ .../ServiceCollectionExtensions.cs | 109 +++++---- .../Services/HashService.cs | 10 +- .../Services/KDFService.cs | 22 ++ 16 files changed, 457 insertions(+), 301 deletions(-) create mode 100644 AdvancedSystems.Security.Abstractions/IKDFService.cs create mode 100644 AdvancedSystems.Security.Tests/Cryptography/KDFProviderTests.cs create mode 100644 AdvancedSystems.Security.Tests/Fixtures/KDFServiceFixture.cs create mode 100644 AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs create mode 100644 AdvancedSystems.Security/Cryptography/KDFProvider.cs create mode 100644 AdvancedSystems.Security/Services/KDFService.cs diff --git a/AdvancedSystems.Security.Abstractions/IHashService.cs b/AdvancedSystems.Security.Abstractions/IHashService.cs index a3b2ab7..cab1229 100644 --- a/AdvancedSystems.Security.Abstractions/IHashService.cs +++ b/AdvancedSystems.Security.Abstractions/IHashService.cs @@ -30,50 +30,5 @@ public interface IHashService /// byte[] Compute(HashFunction hashFunction, byte[] buffer); - /// - /// Attempts to compute a PBKDF2 (password-based key derivation function). - /// This method is suitable for securely hashing passwords. - /// - /// - /// The hash algorithm to use to derive the hash. - /// - /// - /// The password used to derive the hash. - /// - /// - /// The salt used to derive the hash. - /// - /// - /// The size of hash to derive. - /// - /// - /// The number of iterations for the operation. - /// - /// - /// A byte array containing the created PBKDF2 derived hash. - /// - /// - /// if the operation succeeds; otherwise, . - /// - /// - /// Supported algorithms for the parameter are: - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Additionally, some platforms may support SHA3-equivalent hash functions. - /// - bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, out byte[]? pbkdf2); - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Abstractions/IKDFService.cs b/AdvancedSystems.Security.Abstractions/IKDFService.cs new file mode 100644 index 0000000..0804187 --- /dev/null +++ b/AdvancedSystems.Security.Abstractions/IKDFService.cs @@ -0,0 +1,57 @@ +namespace AdvancedSystems.Security.Abstractions; + +/// +/// Represents a contract employing for key derivation functions. +/// +/// +public interface IKDFService +{ + #region Methods + + /// + /// Attempts to compute a PBKDF2 (password-based key derivation function). + /// This method is suitable for securely hashing passwords. + /// + /// + /// The hash algorithm to use to derive the hash. + /// + /// + /// The password used to derive the hash. + /// + /// + /// The salt used to derive the hash. + /// + /// + /// The size of hash to derive. + /// + /// + /// The number of iterations for the operation. + /// + /// + /// A byte array containing the created PBKDF2 derived hash. + /// + /// + /// if the operation succeeds; otherwise, . + /// + /// + /// Supported algorithms for the parameter are: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Additionally, some platforms may support SHA3-equivalent hash functions. + /// + bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, out byte[]? pbkdf2); + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs index 3c916cc..292508e 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs @@ -171,78 +171,5 @@ public void TestSHA512Hash(string input, string expected, Format format) Assert.Equal(expected, sha512); } - /// - /// Tests that - /// computes the hash code successfully and returns the hash with the expected size using the - /// algorithm. - /// - /// - /// The specified hash function. - /// - /// - /// The salt size to use. - /// - [Theory] - [InlineData(HashFunction.SHA1, 128)] - [InlineData(HashFunction.SHA256, 128)] - [InlineData(HashFunction.SHA384, 128)] - [InlineData(HashFunction.SHA512, 128)] - public void TestTryComputePBKDF2(HashFunction hashFunction, int saltSize) - { - // Arrange - int iterations = 100_000; - int hashSize = hashFunction.GetSize(); - - byte[] password = Encoding.UTF8.GetBytes("secret"); - byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); - - // Act - bool isSuccessful = HashProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out byte[]? hash); - - // Assert - Assert.Multiple(() => - { - Assert.True(isSuccessful); - Assert.Equal(saltSize, salt.Length); - Assert.Equal(hashSize, hash?.Length); - }); - } - - /// - /// Tests that - /// fails to compute the hash code on unsupported values and that the resulting - /// hash is empty. - /// - /// - /// The specified hash function. - /// - /// - /// The salt size to use. - /// - [Theory] - [InlineData(HashFunction.SHA3_256, 128)] - [InlineData(HashFunction.SHA3_384, 128)] - [InlineData(HashFunction.SHA3_512, 128)] - public void TestTryComputePBKDF2_HashFunctionSupport(HashFunction hashFunction, int saltSize) - { - // Arrange - int iterations = 100_000; - int hashSize = hashFunction.GetSize(); - - byte[] password = Encoding.UTF8.GetBytes("secret"); - byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); - - // Act - bool isSuccessful = HashProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out byte[]? hash); - - // Assert - Assert.Multiple(() => - { - // All current platforms support HMAC-SHA3-256, 384, and 512 together, so we can simplify the check - // to just checking HMAC-SHA3-256 for the availability of 384 and 512, too. - Assert.Equal(HMACSHA3_256.IsSupported, isSuccessful); - }); - } - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Cryptography/KDFProviderTests.cs b/AdvancedSystems.Security.Tests/Cryptography/KDFProviderTests.cs new file mode 100644 index 0000000..45ce108 --- /dev/null +++ b/AdvancedSystems.Security.Tests/Cryptography/KDFProviderTests.cs @@ -0,0 +1,91 @@ +using System.Security.Cryptography; +using System.Text; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; + +using Xunit; + +namespace AdvancedSystems.Security.Tests.Cryptography; + +public sealed class KDFProviderTests +{ + #region Tests + + /// + /// Tests that + /// computes the hash code successfully and returns the hash with the expected size using the + /// algorithm. + /// + /// + /// The specified hash function. + /// + /// + /// The salt size to use. + /// + [Theory] + [InlineData(HashFunction.SHA1, 128)] + [InlineData(HashFunction.SHA256, 128)] + [InlineData(HashFunction.SHA384, 128)] + [InlineData(HashFunction.SHA512, 128)] + public void TestTryComputePBKDF2(HashFunction hashFunction, int saltSize) + { + // Arrange + int iterations = 100_000; + int hashSize = hashFunction.GetSize(); + + byte[] password = Encoding.UTF8.GetBytes("secret"); + byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); + + // Act + bool isSuccessful = KDFProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out byte[]? pbkdf2); + + // Assert + Assert.Multiple(() => + { + Assert.True(isSuccessful); + Assert.Equal(saltSize, salt.Length); + Assert.Equal(hashSize, pbkdf2?.Length); + Assert.NotNull(pbkdf2); + }); + } + + /// + /// Tests that + /// fails to compute the hash code on unsupported values and that the resulting + /// hash is empty. + /// + /// + /// The specified hash function. + /// + /// + /// The salt size to use. + /// + [Theory] + [InlineData(HashFunction.SHA3_256, 128)] + [InlineData(HashFunction.SHA3_384, 128)] + [InlineData(HashFunction.SHA3_512, 128)] + public void TestTryComputePBKDF2_HashFunctionSupport(HashFunction hashFunction, int saltSize) + { + // Arrange + int iterations = 100_000; + int hashSize = hashFunction.GetSize(); + + byte[] password = Encoding.UTF8.GetBytes("secret"); + byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); + + // Act + bool isSuccessful = KDFProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out byte[]? hash); + + // Assert + Assert.Multiple(() => + { + // All current platforms support HMAC-SHA3-256, 384, and 512 together, so we can simplify the check + // to just checking HMAC-SHA3-256 for the availability of 384 and 512, too. + Assert.Equal(HMACSHA3_256.IsSupported, isSuccessful); + }); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index d42d9eb..3332569 100644 --- a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -1,11 +1,8 @@ using System.Security.Cryptography.X509Certificates; -using System.Text; using System.Threading.Tasks; using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.DependencyInjection; -using AdvancedSystems.Security.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; @@ -22,110 +19,41 @@ namespace AdvancedSystems.Security.Tests.DependencyInjection; /// public sealed class ServiceCollectionExtensionsTests { - #region AddCryptoRandomService Tests + #region AddCertificateService Tests /// - /// Tests that can be initialized through dependency injection. + /// Tests that can be initialized through dependency injection. /// [Fact] - public async Task TestAddCryptoRandomService() + public async Task TestAddCertificateService_FromOptions() { // 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 + string storeService = "my/CurrentUser"; - /// - /// 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(); - byte[]? sha256 = hashService?.Compute(HashFunction.SHA256, buffer); - - // Assert - Assert.Multiple(() => - { - Assert.NotNull(hashService); - Assert.Equal("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", sha256?.ToString(Format.Hex)); - }); - - await hostBuilder.StopAsync(); - } - - #endregion - - #region AddHMACService Tests + services.AddCertificateStore(storeService, options => + { + options.Location = StoreLocation.CurrentUser; + options.Name = StoreName.My; + }); - /// - /// Tests that can be initialized through dependency injection. - /// - [Fact] - public async Task TestAddHMACService() - { - // Arrange - using var hostBuilder = await new HostBuilder() - .ConfigureWebHost(builder => + services.AddCertificateService(); + }) + .Configure(app => { - builder.UseTestServer(); - builder.ConfigureServices(services => - { - services.AddHMACService(); - }); - builder.Configure(app => - { - }); - }) + })) .StartAsync(); // Act - var cryptoRandomService = hostBuilder.Services.GetService(); + var certificateService = hostBuilder.Services.GetService(); // Assert - Assert.NotNull(cryptoRandomService); + Assert.NotNull(certificateService); await hostBuilder.StopAsync(); } @@ -204,48 +132,133 @@ public async Task TestAddCertificateStore_FromAppSettings() #endregion - #region AddCertificateService Tests + #region AddCryptoRandomService Tests /// - /// Tests that can be initialized through dependency injection. + /// Tests that can be initialized through dependency injection. /// [Fact] - public async Task TestAddCertificateService_FromOptions() + public async Task TestAddCryptoRandomService() { // Arrange - string storeService = "my/CurrentUser"; - string thumbprint = "2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC"; + 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.AddCertificateStore(storeService, options => - { - options.Location = StoreLocation.CurrentUser; - options.Name = StoreName.My; - }); + services.AddHashService(); + }) + .Configure(app => + { - services.AddCertificateService(); + })) + .StartAsync(); + + // Act + var hashService = hostBuilder.Services.GetService(); + + // Assert + Assert.NotNull(hashService); + await hostBuilder.StopAsync(); + } + + #endregion + + #region AddKDFService Tests + + /// + /// Tests that can be initialized through dependency injection. + /// + [Fact] + public async Task TestAddKDFService() + { + // Arrange + using var hostBuilder = await new HostBuilder() + .ConfigureWebHost(builder => builder + .UseTestServer() + .ConfigureServices(services => + { + services.AddKDFService(); }) - .Configure(app => + .Configure(app => + { + + })) + .StartAsync(); + + // Act + var kdfService = hostBuilder.Services.GetService(); + + // Assert + Assert.NotNull(kdfService); + await hostBuilder.StopAsync(); + } + + #endregion + + #region AddHMACService Tests + + /// + /// Tests that can be initialized through dependency injection. + /// + [Fact] + public async Task TestAddHMACService() + { + // Arrange + using var hostBuilder = await new HostBuilder() + .ConfigureWebHost(builder => { + builder.UseTestServer(); + builder.ConfigureServices(services => + { + services.AddHMACService(); + }); + builder.Configure(app => + { - })) + }); + }) .StartAsync(); // Act - var certificateService = hostBuilder.Services.GetService(); - var certificate = certificateService?.GetCertificate(storeService, thumbprint, validOnly: false); + var cryptoRandomService = hostBuilder.Services.GetService(); // Assert - Assert.Multiple(() => - { - Assert.NotNull(certificateService); - Assert.NotNull(certificate); - }); - + Assert.NotNull(cryptoRandomService); await hostBuilder.StopAsync(); } diff --git a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs index dec7af7..a7717d9 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs @@ -13,7 +13,7 @@ namespace AdvancedSystems.Security.Tests.Fixtures; -public class CertificateFixture : IAsyncLifetime +public sealed class CertificateFixture : IAsyncLifetime { #region Properties diff --git a/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs index fbe701d..d64cd4e 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs @@ -4,7 +4,7 @@ namespace AdvancedSystems.Security.Tests.Fixtures; -public class CertificateStoreFixture +public sealed class CertificateStoreFixture { public CertificateStoreFixture() { diff --git a/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs index 52c6e6e..9540026 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs @@ -3,7 +3,7 @@ namespace AdvancedSystems.Security.Tests.Fixtures; -public class CryptoRandomFixture +public sealed class CryptoRandomFixture { public CryptoRandomFixture() { diff --git a/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs index 173bd70..3d77817 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs @@ -7,7 +7,7 @@ namespace AdvancedSystems.Security.Tests.Fixtures; -public class HashServiceFixture +public sealed class HashServiceFixture { public HashServiceFixture() { diff --git a/AdvancedSystems.Security.Tests/Fixtures/KDFServiceFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/KDFServiceFixture.cs new file mode 100644 index 0000000..6a5f3d9 --- /dev/null +++ b/AdvancedSystems.Security.Tests/Fixtures/KDFServiceFixture.cs @@ -0,0 +1,18 @@ +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Services; + +namespace AdvancedSystems.Security.Tests.Fixtures; + +public sealed class KDFServiceFixture +{ + public KDFServiceFixture() + { + this.KDFService = new KDFService(); + } + + #region Properties + + public IKDFService KDFService { get; private set; } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs b/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs new file mode 100644 index 0000000..2683601 --- /dev/null +++ b/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs @@ -0,0 +1,50 @@ +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; +using AdvancedSystems.Security.Tests.Fixtures; + +using Xunit; + +namespace AdvancedSystems.Security.Tests.Services; + +/// +/// Tests the public methods in . +/// +public sealed class KDFServiceTests : IClassFixture +{ + private readonly IKDFService _sut; + + public KDFServiceTests(KDFServiceFixture kdfServiceFixture) + { + this._sut = kdfServiceFixture.KDFService; + } + + #region Tests + + /// + /// Tests that + /// returns a non-empty hash with success state . + /// + [Fact] + public void TestTryComputePBKDF2() + { + // Arrange + var sha256 = HashFunction.SHA256; + int iterations = 30_000; + int saltSize = 128; + byte[] password = "REDACTED".GetBytes(Format.String); + byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); + + // Act + bool success = this._sut.TryComputePBKDF2(sha256, password, salt, sha256.GetSize(), iterations, out byte[]? pbkdf2); + + // Assert + Assert.Multiple(() => + { + Assert.True(success); + Assert.NotNull(pbkdf2); + }); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Cryptography/HashProvider.cs b/AdvancedSystems.Security/Cryptography/HashProvider.cs index f378ddd..a96fa9b 100644 --- a/AdvancedSystems.Security/Cryptography/HashProvider.cs +++ b/AdvancedSystems.Security/Cryptography/HashProvider.cs @@ -1,9 +1,7 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using AdvancedSystems.Security.Abstractions; -using AdvancedSystems.Security.Extensions; namespace AdvancedSystems.Security.Cryptography; @@ -28,19 +26,4 @@ public static byte[] Compute(HashFunction hashFunction, byte[] buffer) _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."), }; } - - /// - public static bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2) - { - try - { - pbkdf2 = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, hashFunction.ToHashAlgorithmName(), hashSize); - return true; - } - catch (Exception) - { - pbkdf2 = null; - return false; - } - } } \ No newline at end of file diff --git a/AdvancedSystems.Security/Cryptography/KDFProvider.cs b/AdvancedSystems.Security/Cryptography/KDFProvider.cs new file mode 100644 index 0000000..41dbc5f --- /dev/null +++ b/AdvancedSystems.Security/Cryptography/KDFProvider.cs @@ -0,0 +1,29 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Extensions; + +namespace AdvancedSystems.Security.Cryptography; + +/// +/// Represents a class designed for employing key derivation functions. +/// +public static class KDFProvider +{ + /// + public static bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2) + { + try + { + pbkdf2 = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, hashFunction.ToHashAlgorithmName(), hashSize); + return true; + } + catch (Exception) + { + pbkdf2 = null; + return false; + } + } +} \ No newline at end of file diff --git a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs index 2592db5..7897349 100644 --- a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs @@ -14,29 +14,10 @@ namespace AdvancedSystems.Security.DependencyInjection; 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()); - return services; - } - - #endregion - - #region HashService + #region CertificateService /// - /// Adds the default implementation of to . + /// Adds the default implementation of to . /// /// /// The service collection containing the service. @@ -44,28 +25,10 @@ public static IServiceCollection AddCryptoRandomService(this IServiceCollection /// /// The value of . /// - public static IServiceCollection AddHashService(this IServiceCollection services) + public static IServiceCollection AddCertificateService(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Scoped()); - return services; - } - - #endregion - - #region HMACService + services.TryAdd(ServiceDescriptor.Scoped()); - /// - /// Adds the default implementation of to . - /// - /// - /// The service collection containing the service. - /// - /// - /// The value of . - /// - public static IServiceCollection AddHMACService(this IServiceCollection services) - { - services.TryAdd(ServiceDescriptor.Scoped()); return services; } @@ -132,10 +95,10 @@ public static IServiceCollection AddCertificateStore(this IServiceCollection ser #endregion - #region CertificateService + #region CryptoRandomService /// - /// Adds the default implementation of to . + /// Adds the default implementation of to . /// /// /// The service collection containing the service. @@ -143,10 +106,66 @@ public static IServiceCollection AddCertificateStore(this IServiceCollection ser /// /// The value of . /// - public static IServiceCollection AddCertificateService(this IServiceCollection services) + public static IServiceCollection AddCryptoRandomService(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Scoped()); + services.TryAdd(ServiceDescriptor.Scoped()); + return services; + } + + #endregion + + #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()); + return services; + } + #endregion + + #region HMACService + + /// + /// Adds the default implementation of to . + /// + /// + /// The service collection containing the service. + /// + /// + /// The value of . + /// + public static IServiceCollection AddHMACService(this IServiceCollection services) + { + services.TryAdd(ServiceDescriptor.Scoped()); + return services; + } + + #endregion + + #region KDFService + + /// + /// Adds the default implementation of to . + /// + /// + /// The service collection containing the service. + /// + /// + /// The value of . + /// + public static IServiceCollection AddKDFService(this IServiceCollection services) + { + services.TryAdd(ServiceDescriptor.Scoped()); return services; } diff --git a/AdvancedSystems.Security/Services/HashService.cs b/AdvancedSystems.Security/Services/HashService.cs index 9b6f34f..52e35f8 100644 --- a/AdvancedSystems.Security/Services/HashService.cs +++ b/AdvancedSystems.Security/Services/HashService.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.CodeAnalysis; - -using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Extensions; @@ -36,11 +34,5 @@ public byte[] Compute(HashFunction hashFunction, byte[] buffer) return HashProvider.Compute(hashFunction, buffer); } - /// - public bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2) - { - return HashProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out pbkdf2); - } - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security/Services/KDFService.cs b/AdvancedSystems.Security/Services/KDFService.cs new file mode 100644 index 0000000..e445215 --- /dev/null +++ b/AdvancedSystems.Security/Services/KDFService.cs @@ -0,0 +1,22 @@ +using System.Diagnostics.CodeAnalysis; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Cryptography; + +namespace AdvancedSystems.Security.Services; + +/// +/// Represents a service designed for employing key derivation functions. +/// +public sealed class KDFService : IKDFService +{ + #region Methods + + /// + public bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2) + { + return KDFProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out pbkdf2); + } + + #endregion +} \ No newline at end of file From 079dd81c455c0a68fd5916d5f4fe8608303b4d81 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 5 Feb 2025 19:26:22 +0100 Subject: [PATCH 38/41] Use IFileSystem abstraction in CertificateService --- AdvancedSystems.Security/AdvancedSystems.Security.csproj | 1 + .../DependencyInjection/ServiceCollectionExtensions.cs | 2 ++ AdvancedSystems.Security/Services/CertificateService.cs | 8 +++++--- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/AdvancedSystems.Security/AdvancedSystems.Security.csproj b/AdvancedSystems.Security/AdvancedSystems.Security.csproj index 60a8bdc..232966d 100644 --- a/AdvancedSystems.Security/AdvancedSystems.Security.csproj +++ b/AdvancedSystems.Security/AdvancedSystems.Security.csproj @@ -16,6 +16,7 @@ + diff --git a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs index 7897349..714f54c 100644 --- a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.IO.Abstractions; using AdvancedSystems.Core.DependencyInjection; using AdvancedSystems.Security.Abstractions; @@ -27,6 +28,7 @@ public static partial class ServiceCollectionExtensions /// public static IServiceCollection AddCertificateService(this IServiceCollection services) { + services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Scoped()); return services; diff --git a/AdvancedSystems.Security/Services/CertificateService.cs b/AdvancedSystems.Security/Services/CertificateService.cs index e954cfb..e570f05 100644 --- a/AdvancedSystems.Security/Services/CertificateService.cs +++ b/AdvancedSystems.Security/Services/CertificateService.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -23,11 +23,13 @@ public sealed class CertificateService : ICertificateService { private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; + private readonly IFileSystem _fileSystem; - public CertificateService(ILogger logger, IServiceProvider serviceProvider) + public CertificateService(ILogger logger, IServiceProvider serviceProvider, IFileSystem fileSystem) { this._logger = logger; this._serviceProvider = serviceProvider; + this._fileSystem = fileSystem; } #region Methods @@ -79,7 +81,7 @@ public bool TryImportPemCertificate(string storeService, string certificatePath, if (!string.IsNullOrEmpty(privateKeyPath)) { - string pemContent = File.ReadAllText(privateKeyPath); + string pemContent = this._fileSystem.File.ReadAllText(privateKeyPath); using var privateKey = RSA.Create(); if (withPassword) From 42a2bd15817e4273810a65d9fcc1172fc7aa5178 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 5 Feb 2025 20:46:50 +0100 Subject: [PATCH 39/41] Add libsodium to this project via interop --- .../Interop/LibsodiumTests.cs | 69 +++++++++++++++++++ .../AdvancedSystems.Security.csproj | 1 + .../Cryptography/Libsodium.cs | 52 ++++++++++++++ AdvancedSystems.Security/Interop/Libsodium.cs | 16 +++++ .../Interop/NativeLibrary.cs | 6 ++ 5 files changed, 144 insertions(+) create mode 100644 AdvancedSystems.Security.Tests/Interop/LibsodiumTests.cs create mode 100644 AdvancedSystems.Security/Cryptography/Libsodium.cs create mode 100644 AdvancedSystems.Security/Interop/Libsodium.cs create mode 100644 AdvancedSystems.Security/Interop/NativeLibrary.cs diff --git a/AdvancedSystems.Security.Tests/Interop/LibsodiumTests.cs b/AdvancedSystems.Security.Tests/Interop/LibsodiumTests.cs new file mode 100644 index 0000000..4314d8c --- /dev/null +++ b/AdvancedSystems.Security.Tests/Interop/LibsodiumTests.cs @@ -0,0 +1,69 @@ +using System; + +using AdvancedSystems.Security.Cryptography; + +using Xunit; + +namespace AdvancedSystems.Security.Tests.Interop; + +/// +/// Tests that the interop code from libsodium can be invoked without +/// failure through the class. +/// +public sealed class LibsodiumTests +{ + #region Tests + + /// + /// Tests that returns the latest stable + /// version of the libsodium NuGet package. + /// + [Fact] + public void TestVersion() + { + // Arrange + var expectedVersion = new Version(1, 0, 20); + + // Act + Version actualVersion = Libsodium.Version; + + // Assert + Assert.Equal(expectedVersion, actualVersion); + } + + /// + /// Tests that returns the latest + /// major version of the underlying libsodium DLL. + /// + [Fact] + public void TestMajorVersion() + { + // Arrange + int expectedMajorVersion = 26; + + // Act + int actualMajorVersion = Libsodium.MajorVersion; + + // Assert + Assert.Equal(expectedMajorVersion, actualMajorVersion); + } + + /// + /// Tests that returns the latest + /// minor version of the underlying libsodium DLL. + /// + [Fact] + public void TestMinorVersion() + { + // Arrange + int expectedMinorVersion = 2; + + // Act + int actualMinorVersion = Libsodium.MinorVersion; + + // Assert + Assert.Equal(expectedMinorVersion, actualMinorVersion); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security/AdvancedSystems.Security.csproj b/AdvancedSystems.Security/AdvancedSystems.Security.csproj index 232966d..5c765a1 100644 --- a/AdvancedSystems.Security/AdvancedSystems.Security.csproj +++ b/AdvancedSystems.Security/AdvancedSystems.Security.csproj @@ -11,6 +11,7 @@ + diff --git a/AdvancedSystems.Security/Cryptography/Libsodium.cs b/AdvancedSystems.Security/Cryptography/Libsodium.cs new file mode 100644 index 0000000..53b5395 --- /dev/null +++ b/AdvancedSystems.Security/Cryptography/Libsodium.cs @@ -0,0 +1,52 @@ +using System; +using System.Runtime.InteropServices; + +using static AdvancedSystems.Security.Interop.Libsodium; + +namespace AdvancedSystems.Security.Cryptography; + +public static partial class Libsodium +{ + #region Properties + + /// + /// Gets the version number of the underlying libsodium NuGet package. + /// + /// + /// See also: . + /// + public static Version Version + { + get + { + string? version = Marshal.PtrToStringAnsi(sodium_version_string()); + ArgumentNullException.ThrowIfNull(version, nameof(version)); + + return new Version(version); + } + } + + /// + /// Gets the major version number of the underlying native libsodium DLL. + /// + public static int MajorVersion + { + get + { + return sodium_library_version_major(); + } + } + + /// + /// Gets the minor version number of the underlying native libsodium DLL. + /// + public static int MinorVersion + { + get + { + return sodium_library_version_minor(); + } + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Interop/Libsodium.cs b/AdvancedSystems.Security/Interop/Libsodium.cs new file mode 100644 index 0000000..cc33ece --- /dev/null +++ b/AdvancedSystems.Security/Interop/Libsodium.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.InteropServices; + +namespace AdvancedSystems.Security.Interop; + +internal static partial class Libsodium +{ + [DllImport(NativeLibrary.LIBSODIUM, CallingConvention = CallingConvention.Cdecl)] + internal static extern int sodium_library_version_major(); + + [DllImport(NativeLibrary.LIBSODIUM, CallingConvention = CallingConvention.Cdecl)] + internal static extern int sodium_library_version_minor(); + + [DllImport(NativeLibrary.LIBSODIUM, CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr sodium_version_string(); +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Interop/NativeLibrary.cs b/AdvancedSystems.Security/Interop/NativeLibrary.cs new file mode 100644 index 0000000..fa39b6e --- /dev/null +++ b/AdvancedSystems.Security/Interop/NativeLibrary.cs @@ -0,0 +1,6 @@ +namespace AdvancedSystems.Security.Interop; + +internal static partial class NativeLibrary +{ + internal const string LIBSODIUM = "libsodium"; +} \ No newline at end of file From 5980bc3f1c60b5fb2c6e93e1b51f19f539e73531 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 5 Feb 2025 21:25:40 +0100 Subject: [PATCH 40/41] Refactor interop code and use LibraryImport instead of DllImport --- .../Cryptography/Libsodium.cs | 17 +++++++++++++---- AdvancedSystems.Security/Interop/Libsodium.cs | 18 +++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/AdvancedSystems.Security/Cryptography/Libsodium.cs b/AdvancedSystems.Security/Cryptography/Libsodium.cs index 53b5395..c9ee96a 100644 --- a/AdvancedSystems.Security/Cryptography/Libsodium.cs +++ b/AdvancedSystems.Security/Cryptography/Libsodium.cs @@ -5,6 +5,12 @@ namespace AdvancedSystems.Security.Cryptography; +/// +/// A .NET wrapper class that provides bindings to the native libsodium library. +/// +/// +/// See also: . +/// public static partial class Libsodium { #region Properties @@ -12,14 +18,17 @@ public static partial class Libsodium /// /// Gets the version number of the underlying libsodium NuGet package. /// + /// + /// Raised if the native code execution failed to retrieve the version number. + /// /// - /// See also: . + /// /// public static Version Version { get { - string? version = Marshal.PtrToStringAnsi(sodium_version_string()); + string? version = Marshal.PtrToStringAnsi(SodiumVersionString()); ArgumentNullException.ThrowIfNull(version, nameof(version)); return new Version(version); @@ -33,7 +42,7 @@ public static int MajorVersion { get { - return sodium_library_version_major(); + return SodiumLibraryVersionMajor(); } } @@ -44,7 +53,7 @@ public static int MinorVersion { get { - return sodium_library_version_minor(); + return SodiumLibraryVersionMinor(); } } diff --git a/AdvancedSystems.Security/Interop/Libsodium.cs b/AdvancedSystems.Security/Interop/Libsodium.cs index cc33ece..e2c09a9 100644 --- a/AdvancedSystems.Security/Interop/Libsodium.cs +++ b/AdvancedSystems.Security/Interop/Libsodium.cs @@ -3,14 +3,18 @@ namespace AdvancedSystems.Security.Interop; -internal static partial class Libsodium +internal partial class Libsodium { - [DllImport(NativeLibrary.LIBSODIUM, CallingConvention = CallingConvention.Cdecl)] - internal static extern int sodium_library_version_major(); + #region version.c - [DllImport(NativeLibrary.LIBSODIUM, CallingConvention = CallingConvention.Cdecl)] - internal static extern int sodium_library_version_minor(); + [LibraryImport(NativeLibrary.LIBSODIUM, EntryPoint = "sodium_library_version_major")] + internal static partial int SodiumLibraryVersionMajor(); - [DllImport(NativeLibrary.LIBSODIUM, CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr sodium_version_string(); + [LibraryImport(NativeLibrary.LIBSODIUM, EntryPoint = "sodium_library_version_minor")] + internal static partial int SodiumLibraryVersionMinor(); + + [LibraryImport(NativeLibrary.LIBSODIUM, EntryPoint = "sodium_version_string")] + internal static partial IntPtr SodiumVersionString(); + + #endregion } \ No newline at end of file From 87af8c170ed77d92d20aeca8a3038e1eefe1e2ca Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Wed, 5 Feb 2025 22:41:39 +0100 Subject: [PATCH 41/41] Restructure libsodium code --- .../{Libsodium.cs => Libsodium.Version.cs} | 0 .../Interop/{Libsodium.cs => Libsodium.Version.cs} | 10 +++++----- docs/docs/providers.md | 4 ---- 3 files changed, 5 insertions(+), 9 deletions(-) rename AdvancedSystems.Security/Cryptography/{Libsodium.cs => Libsodium.Version.cs} (100%) rename AdvancedSystems.Security/Interop/{Libsodium.cs => Libsodium.Version.cs} (67%) diff --git a/AdvancedSystems.Security/Cryptography/Libsodium.cs b/AdvancedSystems.Security/Cryptography/Libsodium.Version.cs similarity index 100% rename from AdvancedSystems.Security/Cryptography/Libsodium.cs rename to AdvancedSystems.Security/Cryptography/Libsodium.Version.cs diff --git a/AdvancedSystems.Security/Interop/Libsodium.cs b/AdvancedSystems.Security/Interop/Libsodium.Version.cs similarity index 67% rename from AdvancedSystems.Security/Interop/Libsodium.cs rename to AdvancedSystems.Security/Interop/Libsodium.Version.cs index e2c09a9..3d368cb 100644 --- a/AdvancedSystems.Security/Interop/Libsodium.cs +++ b/AdvancedSystems.Security/Interop/Libsodium.Version.cs @@ -1,20 +1,20 @@ using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace AdvancedSystems.Security.Interop; -internal partial class Libsodium +internal static partial class Libsodium { - #region version.c - [LibraryImport(NativeLibrary.LIBSODIUM, EntryPoint = "sodium_library_version_major")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] internal static partial int SodiumLibraryVersionMajor(); [LibraryImport(NativeLibrary.LIBSODIUM, EntryPoint = "sodium_library_version_minor")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] internal static partial int SodiumLibraryVersionMinor(); [LibraryImport(NativeLibrary.LIBSODIUM, EntryPoint = "sodium_version_string")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] internal static partial IntPtr SodiumVersionString(); - - #endregion } \ No newline at end of file diff --git a/docs/docs/providers.md b/docs/docs/providers.md index 87499d8..5046db3 100644 --- a/docs/docs/providers.md +++ b/docs/docs/providers.md @@ -8,10 +8,6 @@ TODO ## Hash -## Generic Hash Algorithms - -## PBKDF2 Derived Keys - TODO ## RSACryptoProvider