From 767ad4be2d8b36d7a77758e58aacd4307b0aeeb1 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 9 Feb 2025 20:44:07 +0100 Subject: [PATCH 01/15] Refactor code amd continue implementation of RSA encryption --- .../IRSACryptoService.cs | 5 +- .../Cryptography/RSACryptoProviderTests.cs | 49 +++++++++++ .../Extensions/CertificateExtensionsTests.cs | 6 +- .../Fixtures/CertificateStoreFixture.cs | 29 ++++++- ...xture.cs => CryptoRandomServiceFixture.cs} | 4 +- .../{HMACFixture.cs => HMACServiceFixture.cs} | 4 +- .../{HashFixture.cs => HashServiceFixture.cs} | 0 .../{CertificateFixture.cs => HostFixture.cs} | 2 +- .../Fixtures/RSACryptoProviderFixture.cs | 43 ++++++++++ .../Helpers/Certificates.cs | 19 +++++ .../Helpers/UserSecrets.cs | 2 +- .../Services/CertificateServiceTests.cs | 23 ++---- .../Services/CryptoRandomServiceTests.cs | 6 +- .../Services/HMACServiceTests.cs | 4 +- .../Services/RSACryptoServiceTests.cs | 49 +++++++++++ .../Cryptography/RSACryptoProvider.cs | 81 +++++++++---------- .../ServiceCollectionExtensions.cs | 4 +- .../Extensions/StringExtensions.cs | 12 --- .../Options/RSACryptoOptions.cs | 10 +-- .../Services/RSACryptoService.cs | 55 ++++++------- 20 files changed, 279 insertions(+), 128 deletions(-) create mode 100644 AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs rename AdvancedSystems.Security.Tests/Fixtures/{CryptoRandomFixture.cs => CryptoRandomServiceFixture.cs} (79%) rename AdvancedSystems.Security.Tests/Fixtures/{HMACFixture.cs => HMACServiceFixture.cs} (80%) rename AdvancedSystems.Security.Tests/Fixtures/{HashFixture.cs => HashServiceFixture.cs} (100%) rename AdvancedSystems.Security.Tests/Fixtures/{CertificateFixture.cs => HostFixture.cs} (96%) create mode 100644 AdvancedSystems.Security.Tests/Fixtures/RSACryptoProviderFixture.cs create mode 100644 AdvancedSystems.Security.Tests/Helpers/Certificates.cs create mode 100644 AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs delete mode 100644 AdvancedSystems.Security/Extensions/StringExtensions.cs diff --git a/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs b/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs index 28bda07..a505e1e 100644 --- a/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs +++ b/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs @@ -1,7 +1,6 @@ using System; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Text; namespace AdvancedSystems.Security.Abstractions; @@ -14,14 +13,12 @@ public interface IRSACryptoService : IDisposable X509Certificate2 Certificate { get; } - HashAlgorithmName HashAlgorithmName { get; } + HashFunction HashFunction { get; } RSAEncryptionPadding EncryptionPadding { get; } RSASignaturePadding SignaturePadding { get; } - Encoding Encoding { get; } - #endregion #region Methods diff --git a/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs b/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs new file mode 100644 index 0000000..f8f27e7 --- /dev/null +++ b/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs @@ -0,0 +1,49 @@ +using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; +using AdvancedSystems.Security.Tests.Fixtures; + +using Xunit; + +namespace AdvancedSystems.Security.Tests.Cryptography; + +/// +/// Tests the public methods in . +/// +public sealed class RSACryptoProviderTests : IClassFixture +{ + private readonly RSACryptoProviderFixture _sut; + + public RSACryptoProviderTests(RSACryptoProviderFixture rsaCryptoProviderFixture) + { + this._sut = rsaCryptoProviderFixture; + } + + #region Tests + + /// + /// Tests that encrypts an array of bytes correctly + /// by using a pre-configured certificate. + /// + [Fact] + public void TestEncryptionDecryption_Roundtrip() + { + // Arrange + string message = "Hello, World!"; + byte[] buffer = message.GetBytes(Format.String); + + // Act + byte[] cipher = this._sut.RSACryptoProvider.Encrypt(buffer); + byte[] source = this._sut.RSACryptoProvider.Decrypt(cipher); + string decryptedMessage = source.ToString(Format.String); + + // Assert + Assert.Multiple(() => + { + Assert.NotEmpty(cipher); + Assert.NotEmpty(source); + Assert.Equal(message, decryptedMessage); + }); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs index 09f42e6..fc2001a 100644 --- a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs @@ -15,11 +15,11 @@ namespace AdvancedSystems.Security.Tests.Extensions; /// /// Tests the public methods in . /// -public sealed class CertificateExtensionsTests : IClassFixture +public sealed class CertificateExtensionsTests : IClassFixture { - private readonly CertificateFixture _certificateFixture; + private readonly HostFixture _certificateFixture; - public CertificateExtensionsTests(CertificateFixture certificateFixture) + public CertificateExtensionsTests(HostFixture certificateFixture) { this._certificateFixture = certificateFixture; } diff --git a/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs index d64cd4e..918b201 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs @@ -1,11 +1,14 @@ -using System.Security.Cryptography.X509Certificates; +using System; +using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Security.Services; namespace AdvancedSystems.Security.Tests.Fixtures; -public sealed class CertificateStoreFixture +public sealed class CertificateStoreFixture : IDisposable { + private bool _isDisposed = false; + public CertificateStoreFixture() { this.CertificateStore = new CertificateStore(StoreName.My, StoreLocation.CurrentUser); @@ -16,4 +19,26 @@ public CertificateStoreFixture() public CertificateStore CertificateStore { get; set; } #endregion + + #region Methods + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + public void Dispose(bool disposing) + { + if (this._isDisposed) return; + + if (disposing) + { + this.CertificateStore.Dispose(); + } + + this._isDisposed = true; + } + + #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomServiceFixture.cs similarity index 79% rename from AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs rename to AdvancedSystems.Security.Tests/Fixtures/CryptoRandomServiceFixture.cs index 9540026..3d5a766 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomServiceFixture.cs @@ -3,9 +3,9 @@ namespace AdvancedSystems.Security.Tests.Fixtures; -public sealed class CryptoRandomFixture +public sealed class CryptoRandomServiceFixture { - public CryptoRandomFixture() + public CryptoRandomServiceFixture() { this.CryptoRandomService = new CryptoRandomService(); } diff --git a/AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/HMACServiceFixture.cs similarity index 80% rename from AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs rename to AdvancedSystems.Security.Tests/Fixtures/HMACServiceFixture.cs index 461f097..a2904d2 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/HMACServiceFixture.cs @@ -3,9 +3,9 @@ namespace AdvancedSystems.Security.Tests.Fixtures; -public sealed class HMACFixture +public sealed class HMACServiceFixture { - public HMACFixture() + public HMACServiceFixture() { this.HMACService = new HMACService(); } diff --git a/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/HashServiceFixture.cs similarity index 100% rename from AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs rename to AdvancedSystems.Security.Tests/Fixtures/HashServiceFixture.cs diff --git a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/HostFixture.cs similarity index 96% rename from AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs rename to AdvancedSystems.Security.Tests/Fixtures/HostFixture.cs index a7717d9..788abda 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/HostFixture.cs @@ -13,7 +13,7 @@ namespace AdvancedSystems.Security.Tests.Fixtures; -public sealed class CertificateFixture : IAsyncLifetime +public sealed class HostFixture : IAsyncLifetime { #region Properties diff --git a/AdvancedSystems.Security.Tests/Fixtures/RSACryptoProviderFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/RSACryptoProviderFixture.cs new file mode 100644 index 0000000..c4ff2fe --- /dev/null +++ b/AdvancedSystems.Security.Tests/Fixtures/RSACryptoProviderFixture.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Services; +using AdvancedSystems.Security.Tests.Helpers; + +namespace AdvancedSystems.Security.Tests.Fixtures; + +public sealed class RSACryptoProviderFixture : IDisposable +{ + public RSACryptoProviderFixture() + { + using var store = new CertificateStore(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadOnly); + + this.PasswordCertificate = store.Certificates + .Find(X509FindType.FindByThumbprint, Certificates.PasswordCertificateThumbprint, validOnly: false) + .OfType() + .First(); + + this.RSACryptoProvider = new RSACryptoProvider(this.PasswordCertificate); + } + + #region Properties + + public X509Certificate2 PasswordCertificate { get; private set; } + + public RSACryptoProvider RSACryptoProvider { get; private set; } + + #endregion + + #region Methods + + public void Dispose() + { + this.PasswordCertificate.Dispose(); + this.RSACryptoProvider.Dispose(); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Helpers/Certificates.cs b/AdvancedSystems.Security.Tests/Helpers/Certificates.cs new file mode 100644 index 0000000..ace7269 --- /dev/null +++ b/AdvancedSystems.Security.Tests/Helpers/Certificates.cs @@ -0,0 +1,19 @@ +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace AdvancedSystems.Security.Tests.Helpers; + +internal static class Certificates +{ + internal const string PasswordCertificateThumbprint = "2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC"; + + internal 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; + } +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs b/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs index 6c7605f..cc1dc23 100644 --- a/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs +++ b/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs @@ -5,5 +5,5 @@ internal static class UserSecrets /// /// Retrieves the certificate password from the secrets store. /// - public const string CERTIFICATE_PASSWORD = "CertificatePassword"; + internal 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 e4ad01d..7fe00e2 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs @@ -18,11 +18,11 @@ namespace AdvancedSystems.Security.Tests.Services; /// /// Tests the public methods in . /// -public sealed class CertificateServiceTests : IClassFixture +public sealed class CertificateServiceTests : IClassFixture { - private readonly CertificateFixture _sut; + private readonly HostFixture _sut; - public CertificateServiceTests(CertificateFixture certificateFixture) + public CertificateServiceTests(HostFixture certificateFixture) { this._sut = certificateFixture; } @@ -123,7 +123,7 @@ public void TestGetCertificate_ByThumbprint() { // Arrange string storeService = this._sut.ConfiguredStoreService; - string thumbprint = "2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC"; + string thumbprint = Certificates.PasswordCertificateThumbprint; // Act var certificateService = this._sut.Host?.Services.GetService(); @@ -148,7 +148,7 @@ public void TestAddRemoveCertificate() { // Arrange string storeService = this._sut.ConfiguredStoreService; - var certificate1 = CreateSelfSignedCertificate("O=AdvancedSystems"); + var certificate1 = Certificates.CreateSelfSignedCertificate("O=AdvancedSystems"); string thumbprint = certificate1.Thumbprint; // Act @@ -168,17 +168,4 @@ public void TestAddRemoveCertificate() } #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 diff --git a/AdvancedSystems.Security.Tests/Services/CryptoRandomServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CryptoRandomServiceTests.cs index df2f796..e10f2a1 100644 --- a/AdvancedSystems.Security.Tests/Services/CryptoRandomServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CryptoRandomServiceTests.cs @@ -14,11 +14,11 @@ namespace AdvancedSystems.Security.Tests.Services; /// /// These methods are more exhaustively tested by the underlying provider class. /// -public sealed class CryptoRandomServiceTests : IClassFixture +public sealed class CryptoRandomServiceTests : IClassFixture { - private readonly CryptoRandomFixture _sut; + private readonly CryptoRandomServiceFixture _sut; - public CryptoRandomServiceTests(CryptoRandomFixture fixture) + public CryptoRandomServiceTests(CryptoRandomServiceFixture fixture) { this._sut = fixture; } diff --git a/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs index 7c773ca..17c1c74 100644 --- a/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs @@ -12,12 +12,12 @@ namespace AdvancedSystems.Security.Tests.Services; /// /// Tests the public methods in . /// -public sealed class HMACServiceTests : IClassFixture, IClassFixture +public sealed class HMACServiceTests : IClassFixture, IClassFixture { private readonly IHMACService _sut; private readonly ICryptoRandomService _cryptoRandomService; - public HMACServiceTests(HMACFixture fixture, CryptoRandomFixture cryptoRandomFixture) + public HMACServiceTests(HMACServiceFixture fixture, CryptoRandomServiceFixture cryptoRandomFixture) { this._sut = fixture.HMACService; this._cryptoRandomService = cryptoRandomFixture.CryptoRandomService; diff --git a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs new file mode 100644 index 0000000..7bbdd78 --- /dev/null +++ b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs @@ -0,0 +1,49 @@ +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Services; +using AdvancedSystems.Security.Tests.Fixtures; +using AdvancedSystems.Security.Tests.Helpers; + +using Microsoft.Extensions.DependencyInjection; + +using Xunit; + +namespace AdvancedSystems.Security.Tests.Services; + +/// +/// Tests the public methods in . +/// +public sealed class RSACryptoServiceTests : IClassFixture +{ + private readonly HostFixture _certificateFixture; + + public RSACryptoServiceTests(HostFixture certificateFixture) + { + this._certificateFixture = certificateFixture; + } + + #region Tests + + /// + /// Tests that encrypts an array of bytes correctly + /// by using a pre-configured certificate. + /// + [Fact] + public void TestEncryptionDecryption_Roundtrip() + { + // Arrange + string storeService = this._certificateFixture.ConfiguredStoreService; + string thumbprint = Certificates.PasswordCertificateThumbprint; + + // Act + ICertificateService? certificateService = this._certificateFixture.Host?.Services.GetService(); + var certificate = certificateService?.GetCertificate(storeService, thumbprint, validOnly: false); + + // Assert + Assert.Multiple(() => + { + Assert.NotNull(certificateService); + }); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs index 656910e..8cf95ab 100644 --- a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs +++ b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs @@ -1,112 +1,109 @@ using System; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Text; -using AdvancedSystems.Core.Extensions; +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Extensions; namespace AdvancedSystems.Security.Cryptography; /// /// Represents a class for performing RSA-based asymmetric operations. /// -public sealed class RSACryptoProvider +public sealed class RSACryptoProvider : IDisposable { - private static readonly HashAlgorithmName DEFAULT_HASH_ALGORITHM_NAME = HashAlgorithmName.SHA256; + private bool _isDisposed = false; + + private static readonly HashFunction DEFAULT_HASH_FUNCTION = HashFunction.SHA256; private static readonly RSAEncryptionPadding DEFAULT_RSA_ENCRYPTION_PADDING = RSAEncryptionPadding.OaepSHA256; private static readonly RSASignaturePadding DEFAULT_RSA_SIGNATURE_PADDING = RSASignaturePadding.Pss; - private static readonly Encoding DEFAULT_ENCODING = Encoding.UTF8; - public RSACryptoProvider(X509Certificate2 certificate, HashAlgorithmName hashAlgorithm, RSAEncryptionPadding encryptionPadding, RSASignaturePadding signaturePadding, Encoding encoding) + public RSACryptoProvider(X509Certificate2 certificate, HashFunction hashFunction, RSAEncryptionPadding encryptionPadding, RSASignaturePadding signaturePadding) { this.Certificate = certificate; - this.HashAlgorithmName = hashAlgorithm; + this.HashFunction = hashFunction; this.EncryptionPadding = encryptionPadding; this.SignaturePadding = signaturePadding; - this.Encoding = encoding; } public RSACryptoProvider(X509Certificate2 certificate) { this.Certificate = certificate; - this.HashAlgorithmName = RSACryptoProvider.DEFAULT_HASH_ALGORITHM_NAME; - this.EncryptionPadding = RSACryptoProvider.DEFAULT_RSA_ENCRYPTION_PADDING; - this.SignaturePadding = RSACryptoProvider.DEFAULT_RSA_SIGNATURE_PADDING; - this.Encoding = RSACryptoProvider.DEFAULT_ENCODING; + this.HashFunction = DEFAULT_HASH_FUNCTION; + this.EncryptionPadding = DEFAULT_RSA_ENCRYPTION_PADDING; + this.SignaturePadding = DEFAULT_RSA_SIGNATURE_PADDING; } #region Properties public X509Certificate2 Certificate { get; private set; } - public HashAlgorithmName HashAlgorithmName { get; set; } + public HashFunction HashFunction { get; set; } public RSAEncryptionPadding EncryptionPadding { get; set; } public RSASignaturePadding SignaturePadding { get; set; } - public Encoding Encoding { get; set; } - #endregion #region Public Methods - public string Encrypt(string message, Encoding? encoding = null) + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + public void Dispose(bool disposing) { - encoding ??= this.Encoding; + if (this._isDisposed) return; + if (disposing && this.Certificate is not null) + { + this.Certificate.Dispose(); + } + + this._isDisposed = true; + } + + public byte[] Encrypt(byte[] buffer) + { using RSA? publicKey = this.Certificate.GetRSAPublicKey(); ArgumentNullException.ThrowIfNull(publicKey, nameof(publicKey)); - byte[] buffer = encoding.GetBytes(message); byte[] cipher = publicKey.Encrypt(buffer, this.EncryptionPadding); - return Convert.ToBase64String(cipher); + return cipher; } - public string Decrypt(string cipher, Encoding? encoding = null) + public byte[] Decrypt(byte[] cipher) { if (!this.Certificate.HasPrivateKey) { throw new CryptographicException($"Certificate with thumbprint '{this.Certificate.Thumbprint}' has no private key."); } - encoding ??= this.Encoding; - using RSA? privateKey = this.Certificate.GetRSAPrivateKey(); ArgumentNullException.ThrowIfNull(privateKey, nameof(privateKey)); - byte[] buffer = Convert.FromBase64String(cipher); - byte[] source = privateKey.Decrypt(buffer, this.EncryptionPadding); - return encoding.GetString(source); + byte[] source = privateKey.Decrypt(cipher, this.EncryptionPadding); + return source; } - public string SignData(string data, Encoding? encoding = null) + public byte[] SignData(byte[] data) { - if (data.IsNullOrEmpty()) - { - throw new ArgumentNullException(nameof(data)); - } - using RSA? privateKey = this.Certificate.GetRSAPrivateKey(); ArgumentNullException.ThrowIfNull(privateKey, nameof(privateKey)); - encoding ??= this.Encoding; - byte[] buffer = encoding.GetBytes(data); - - byte[] signature = privateKey.SignData(buffer, this.HashAlgorithmName, this.SignaturePadding); - return Convert.ToBase64String(signature); + byte[] signature = privateKey.SignData(data, this.HashFunction.ToHashAlgorithmName(), this.SignaturePadding); + return signature; } - public bool VerifyData(string data, string signature, Encoding? encoding = null) + public bool VerifyData(byte[] data, byte[] signature) { using RSA? publicKey = this.Certificate.GetRSAPublicKey(); ArgumentNullException.ThrowIfNull(publicKey, nameof(publicKey)); - encoding ??= this.Encoding; - byte[] buffer = encoding.GetBytes(data); - byte[] signedBuffer = Convert.FromBase64String(signature); - - bool isVerified = publicKey.VerifyData(buffer, signedBuffer, this.HashAlgorithmName, this.SignaturePadding); + bool isVerified = publicKey.VerifyData(data, signature, this.HashFunction.ToHashAlgorithmName(), this.SignaturePadding); return isVerified; } diff --git a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs index 714f54c..ed22c98 100644 --- a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs @@ -177,7 +177,9 @@ public static IServiceCollection AddKDFService(this IServiceCollection services) private static IServiceCollection AddRSACryptoService(this IServiceCollection services) { - throw new NotImplementedException(); + services.TryAdd(ServiceDescriptor.Singleton()); + + return services; } /// diff --git a/AdvancedSystems.Security/Extensions/StringExtensions.cs b/AdvancedSystems.Security/Extensions/StringExtensions.cs deleted file mode 100644 index 1aba33b..0000000 --- a/AdvancedSystems.Security/Extensions/StringExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -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/Options/RSACryptoOptions.cs b/AdvancedSystems.Security/Options/RSACryptoOptions.cs index 6cbea4a..fda31b0 100644 --- a/AdvancedSystems.Security/Options/RSACryptoOptions.cs +++ b/AdvancedSystems.Security/Options/RSACryptoOptions.cs @@ -1,13 +1,14 @@ using System.ComponentModel.DataAnnotations; using System.Security.Cryptography; -using System.Text; + +using AdvancedSystems.Security.Abstractions; namespace AdvancedSystems.Security.Options; public sealed record RSACryptoOptions { [Required] - public required HashAlgorithmName HashAlgorithmName { get; set; } + public required HashFunction HashFunction { get; set; } [Required] public required RSAEncryptionPadding EncryptionPadding { get; set; } @@ -15,9 +16,8 @@ public sealed record RSACryptoOptions [Required] public required RSASignaturePadding SignaturePadding { get; set; } - [Required] - public required Encoding Encoding { get; set; } - [Required] public required string Thumbprint { get; set; } + + public bool RequireValidCertificate { get; set; } = true; } \ No newline at end of file diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs index 2fb4368..31bf418 100644 --- a/AdvancedSystems.Security/Services/RSACryptoService.cs +++ b/AdvancedSystems.Security/Services/RSACryptoService.cs @@ -1,7 +1,6 @@ using System; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Text; using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; @@ -15,13 +14,14 @@ namespace AdvancedSystems.Security.Services; /// /// Represents a service for performing RSA-based asymmetric operations. /// -public sealed class RSACryptoService +public sealed class RSACryptoService : IRSACryptoService { + private bool _isDisposed = false; + private readonly ILogger _logger; private readonly ICertificateService _certificateService; private readonly RSACryptoOptions _rsaOptions; - private bool _disposed = false; private readonly X509Certificate2 _certificate; private readonly RSACryptoProvider _provider; @@ -32,14 +32,13 @@ public RSACryptoService(ILogger logger, ICertificateService ce this._rsaOptions = rsaOptions.Value; this._certificate = this._certificateService.GetCertificate("default", this._rsaOptions.Thumbprint, validOnly: true) - ?? throw new ArgumentNullException(); + ?? throw new ArgumentNullException(nameof(rsaOptions)); this._provider = new RSACryptoProvider( this._certificate, - this._rsaOptions.HashAlgorithmName, + this._rsaOptions.HashFunction, this._rsaOptions.EncryptionPadding, - this._rsaOptions.SignaturePadding, - this._rsaOptions.Encoding + this._rsaOptions.SignaturePadding ); } @@ -55,11 +54,11 @@ public X509Certificate2 Certificate } /// - public HashAlgorithmName HashAlgorithmName + public HashFunction HashFunction { get { - return this._provider.HashAlgorithmName; + return this._provider.HashFunction; } } @@ -81,15 +80,6 @@ public RSASignaturePadding SignaturePadding } } - /// - public Encoding Encoding - { - get - { - return this._provider.Encoding; - } - } - #endregion #region Methods @@ -102,37 +92,42 @@ public void Dispose() GC.SuppressFinalize(this); } - /// + /// public void Dispose(bool disposing) { - if (this._disposed || !disposing) return; + if (this._isDisposed) return; + + if (disposing) + { + this.Certificate.Dispose(); + this._provider.Dispose(); + } - this._certificate.Dispose(); - this._disposed = true; + this._isDisposed = true; } /// - public string Encrypt(string message, Encoding? encoding = null) + public byte[] Encrypt(byte[] buffer) { - return this._provider.Encrypt(message, encoding); + return this._provider.Encrypt(buffer); } /// - public string Decrypt(string cipher, Encoding? encoding = null) + public byte[] Decrypt(byte[] cipher) { - return this._provider.Decrypt(cipher, encoding); + return this._provider.Decrypt(cipher); } /// - public string SignData(string data, Encoding? encoding = null) + public byte[] SignData(byte[] data) { - return this._provider.SignData(data, encoding); + return this._provider.SignData(data); } /// - public bool VerifyData(string data, string signature, Encoding? encoding = null) + public bool VerifyData(byte[] data, byte[] signature) { - return this._provider.VerifyData(data, signature, encoding); + return this._provider.VerifyData(data, signature); } #endregion From c8564a4a2fb582b24d70d77d72e022cb0f54f1a4 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Thu, 13 Feb 2025 23:37:53 +0100 Subject: [PATCH 02/15] Improve documentation for RSA options --- .../Services/CertificateServiceTests.cs | 1 - .../Options/RSACryptoOptions.cs | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs index 7fe00e2..80caf63 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Linq; -using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Security.Abstractions; diff --git a/AdvancedSystems.Security/Options/RSACryptoOptions.cs b/AdvancedSystems.Security/Options/RSACryptoOptions.cs index fda31b0..396923d 100644 --- a/AdvancedSystems.Security/Options/RSACryptoOptions.cs +++ b/AdvancedSystems.Security/Options/RSACryptoOptions.cs @@ -5,19 +5,39 @@ namespace AdvancedSystems.Security.Options; +/// +/// Configures options for the . +/// public sealed record RSACryptoOptions { + /// + /// + /// [Required] + [EnumDataType(typeof(HashFunction))] public required HashFunction HashFunction { get; set; } + /// + /// + /// [Required] public required RSAEncryptionPadding EncryptionPadding { get; set; } + /// + /// + /// [Required] public required RSASignaturePadding SignaturePadding { get; set; } + /// + /// The string representing the thumbprint of the encryption certificate to retrieve. + /// [Required] public required string Thumbprint { get; set; } + /// + /// Set this value to to allow only valid certificates to be + /// used for the encryption and decryption; otherwise, . + /// public bool RequireValidCertificate { get; set; } = true; } \ No newline at end of file From 8849b9a74e3dff33a36dcb56dd82ec928450e505 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 15 Feb 2025 15:17:21 +0100 Subject: [PATCH 03/15] Add executable modifier to Git hooks --- .husky/pre-commit | 0 .husky/pre-push | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .husky/pre-commit mode change 100644 => 100755 .husky/pre-push diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/.husky/pre-push b/.husky/pre-push old mode 100644 new mode 100755 From 0fb787b6caeba67bbaa612990993827fa84e5f3e Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 15 Feb 2025 18:55:24 +0100 Subject: [PATCH 04/15] Add docfx to dotnet tools --- .config/dotnet-tools.json | 7 +++++++ .github/workflows/docs.yml | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index e2a98a3..d039a87 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -22,6 +22,13 @@ "certificate-tool" ], "rollForward": false + }, + "docfx": { + "version": "2.78.2", + "commands": [ + "docfx" + ], + "rollForward": false } } } \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d554ec1..e2414cc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,8 +34,10 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} - - run: dotnet tool update -g docfx - - run: docfx docs/docfx.json + - name: Build Documentation + run: > + dotnet tool restore --configfile nuget.config + dotnet docfx docs/docfx.json - name: Upload Artifact uses: actions/upload-pages-artifact@v3.0.1 From 9501f2c4493d01414abbfcbbc3e7c22f075bec70 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 15 Feb 2025 18:56:48 +0100 Subject: [PATCH 05/15] Create makefile and configure commands for build, test, and documentation --- Makefile | 47 +++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 16 ++++++---------- 2 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d1c3ca6 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +MAIN_PROJECT = ./AdvancedSystems.Security +TEST_PROJECT = ./AdvancedSystems.Security.Tests + +import-certificate: + # password certificate authority + dotnet certificate-tool add --file ./development/AdvancedSystems-CA.pfx \ + --store-name My \ + --store-location CurrentUser \ + --password $(password); + + # password certificate + # TODO: read values from appsettings.json + dotnet certificate-tool add --file ./development/AdvancedSystems-PasswordCertificate.pem \ + --store-name My \ + --store-location CurrentUser; + +install: import-certificate + # main project + dotnet restore --configfile nuget.config + dotnet tool restore --configfile nuget.config + dotnet husky install + + # unit test project + dotnet user-secrets init --project $(TEST_PROJECT) + dotnet user-secrets set CertificatePassword $(password) --project $(TEST_PROJECT) + +build: + dotnet build $(MAIN_PROJECT) --configuration $(configuration) --no-restore /warnAsError + +test: build + dotnet test $(TEST_PROJECT) --configuration $(configuration) --verbosity normal + +documentation: + dotnet tool restore --configfile nuget.config + + if [ "$(serve)" = "true" ]; then \ + dotnet docfx ./docs/docfx.json --serve --open-browser; \ + else \ + dotnet docfx ./docs/docfx.json; \ + fi + +clean: + dotnet clean + dotnet clean --configuration Release + @clear + +$(V).SILENT: \ No newline at end of file diff --git a/readme.md b/readme.md index 764c837..ffd74d5 100644 --- a/readme.md +++ b/readme.md @@ -34,21 +34,17 @@ for debugging .NET assemblies. 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 +$Password = Read-Host "Password" -MaskInput +make install password=$Password ``` Run test suite: ```powershell -dotnet test ./AdvancedSystems.Core.Tests --configuration Release +make test configuration=Release ``` -In addition to unit testing, this project also uses stryker for mutation testing, which is setup to be installed with - -```powershell -dotnet tool restore --configfile nuget.config -``` +In addition to unit testing, this project also uses stryker for mutation testing, which is set up to be installed with Run stryker locally: @@ -56,8 +52,8 @@ Run stryker locally: dotnet stryker ``` -Build and serve documentation locally (`http://localhost:8080`): +Build and serve documentation locally: ```powershell -docfx ./docs/docfx.json --serve +make documentation serve=true ``` From f126972899b3d32e96cff5cab54676259d87c74c Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 16 Feb 2025 20:03:59 +0100 Subject: [PATCH 06/15] Add lint command to Makefile --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index d1c3ca6..8d7194e 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,9 @@ build: test: build dotnet test $(TEST_PROJECT) --configuration $(configuration) --verbosity normal +lint: + dotnet format + documentation: dotnet tool restore --configfile nuget.config From 26a25502f96f712920b1d79cbb3781548559a56e Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sun, 16 Feb 2025 20:05:08 +0100 Subject: [PATCH 07/15] Remove state from RSA provider, fix Dipose implementation and add more unit tests --- .../IRSACryptoService.cs | 22 ++++++++++- .../Cryptography/RSACryptoProviderTests.cs | 23 ++++++++++++ .../Cryptography/RSACryptoProvider.cs | 34 +++++++---------- .../Services/RSACryptoService.cs | 37 +++++++------------ 4 files changed, 70 insertions(+), 46 deletions(-) diff --git a/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs b/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs index a505e1e..276a2ac 100644 --- a/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs +++ b/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs @@ -23,8 +23,26 @@ public interface IRSACryptoService : IDisposable #region Methods - byte[] Encrypt(byte[] message); - + /// + /// Encrypts the input . + /// + /// + /// The data to encrypt. + /// + /// + /// The encrypted data. + /// + byte[] Encrypt(byte[] data); + + /// + /// Decrypts the . + /// + /// + /// The data to decrypt. + /// + /// + /// The decrypted data. + /// byte[] Decrypt(byte[] cipher); byte[] SignData(byte[] data); diff --git a/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs b/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs index f8f27e7..ddc1e5c 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs @@ -45,5 +45,28 @@ public void TestEncryptionDecryption_Roundtrip() }); } + /// + /// Tests that signs and verifies an array of bytes + /// correctly by using a pre-configured certificate. + /// + [Fact] + public void TestSigningVerification_Roundtrip() + { + // Arrange + string message = "Hello, World!"; + byte[] buffer = message.GetBytes(Format.String); + + // Act + byte[] signature = this._sut.RSACryptoProvider.SignData(buffer); + bool verified = this._sut.RSACryptoProvider.VerifyData(buffer, signature); + + // Assert + Assert.Multiple((() => + { + Assert.NotEmpty(signature); + Assert.True(verified); + })); + } + #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs index 8cf95ab..6c86336 100644 --- a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs +++ b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs @@ -14,35 +14,25 @@ public sealed class RSACryptoProvider : IDisposable { private bool _isDisposed = false; - private static readonly HashFunction DEFAULT_HASH_FUNCTION = HashFunction.SHA256; - private static readonly RSAEncryptionPadding DEFAULT_RSA_ENCRYPTION_PADDING = RSAEncryptionPadding.OaepSHA256; - private static readonly RSASignaturePadding DEFAULT_RSA_SIGNATURE_PADDING = RSASignaturePadding.Pss; - - public RSACryptoProvider(X509Certificate2 certificate, HashFunction hashFunction, RSAEncryptionPadding encryptionPadding, RSASignaturePadding signaturePadding) + public RSACryptoProvider(X509Certificate2 certificate) { this.Certificate = certificate; - this.HashFunction = hashFunction; - this.EncryptionPadding = encryptionPadding; - this.SignaturePadding = signaturePadding; } - public RSACryptoProvider(X509Certificate2 certificate) + ~RSACryptoProvider() { - this.Certificate = certificate; - this.HashFunction = DEFAULT_HASH_FUNCTION; - this.EncryptionPadding = DEFAULT_RSA_ENCRYPTION_PADDING; - this.SignaturePadding = DEFAULT_RSA_SIGNATURE_PADDING; + this.Dispose(false); } #region Properties public X509Certificate2 Certificate { get; private set; } - public HashFunction HashFunction { get; set; } + public HashFunction HashFunction { get; set; } = HashFunction.SHA256; - public RSAEncryptionPadding EncryptionPadding { get; set; } + public RSAEncryptionPadding EncryptionPadding { get; set; } = RSAEncryptionPadding.OaepSHA256; - public RSASignaturePadding SignaturePadding { get; set; } + public RSASignaturePadding SignaturePadding { get; set; } = RSASignaturePadding.Pss; #endregion @@ -54,11 +44,11 @@ public void Dispose() GC.SuppressFinalize(this); } - public void Dispose(bool disposing) + private void Dispose(bool disposing) { if (this._isDisposed) return; - if (disposing && this.Certificate is not null) + if (disposing) { this.Certificate.Dispose(); } @@ -66,15 +56,17 @@ public void Dispose(bool disposing) this._isDisposed = true; } - public byte[] Encrypt(byte[] buffer) + /// + public byte[] Encrypt(byte[] data) { using RSA? publicKey = this.Certificate.GetRSAPublicKey(); ArgumentNullException.ThrowIfNull(publicKey, nameof(publicKey)); - byte[] cipher = publicKey.Encrypt(buffer, this.EncryptionPadding); + byte[] cipher = publicKey.Encrypt(data, this.EncryptionPadding); return cipher; } + /// public byte[] Decrypt(byte[] cipher) { if (!this.Certificate.HasPrivateKey) @@ -89,6 +81,7 @@ public byte[] Decrypt(byte[] cipher) return source; } + /// public byte[] SignData(byte[] data) { using RSA? privateKey = this.Certificate.GetRSAPrivateKey(); @@ -98,6 +91,7 @@ public byte[] SignData(byte[] data) return signature; } + /// public bool VerifyData(byte[] data, byte[] signature) { using RSA? publicKey = this.Certificate.GetRSAPublicKey(); diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs index 31bf418..cf477b0 100644 --- a/AdvancedSystems.Security/Services/RSACryptoService.cs +++ b/AdvancedSystems.Security/Services/RSACryptoService.cs @@ -17,41 +17,32 @@ namespace AdvancedSystems.Security.Services; public sealed class RSACryptoService : IRSACryptoService { private bool _isDisposed = false; - private readonly ILogger _logger; - private readonly ICertificateService _certificateService; - private readonly RSACryptoOptions _rsaOptions; - - private readonly X509Certificate2 _certificate; private readonly RSACryptoProvider _provider; public RSACryptoService(ILogger logger, ICertificateService certificateService, IOptions rsaOptions) { this._logger = logger; - this._certificateService = certificateService; - this._rsaOptions = rsaOptions.Value; + RSACryptoOptions rsaOptions1 = rsaOptions.Value; - this._certificate = this._certificateService.GetCertificate("default", this._rsaOptions.Thumbprint, validOnly: true) + this.Certificate = certificateService.GetCertificate("default", rsaOptions1.Thumbprint, validOnly: true) ?? throw new ArgumentNullException(nameof(rsaOptions)); - this._provider = new RSACryptoProvider( - this._certificate, - this._rsaOptions.HashFunction, - this._rsaOptions.EncryptionPadding, - this._rsaOptions.SignaturePadding - ); + this._provider = new RSACryptoProvider(this.Certificate); + this._provider.HashFunction = rsaOptions1.HashFunction; + this._provider.EncryptionPadding = rsaOptions1.EncryptionPadding; + this._provider.SignaturePadding = rsaOptions1.SignaturePadding; + } + + ~RSACryptoService() + { + this.Dispose(false); } #region Properties /// - public X509Certificate2 Certificate - { - get - { - return this._certificate; - } - } + public X509Certificate2 Certificate { get; } /// public HashFunction HashFunction @@ -85,15 +76,13 @@ public RSASignaturePadding SignaturePadding #region Methods /// - public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } - /// - public void Dispose(bool disposing) + private void Dispose(bool disposing) { if (this._isDisposed) return; From 06d06efb7e98776a406e5242477febd910241da9 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 18 Jul 2025 20:22:50 +0200 Subject: [PATCH 08/15] Document RSACryptoContract and clean up code --- .../IRSACryptoService.cs | 53 --------- .../RSACryptoContract.cs | 104 ++++++++++++++++++ .../Services/RSACryptoServiceTests.cs | 2 +- .../Cryptography/RSACryptoProvider.cs | 64 ++++++----- .../ServiceCollectionExtensions.cs | 6 +- .../Extensions/CertificateExtensions.cs | 2 +- .../Options/RSACryptoOptions.cs | 2 +- .../Services/RSACryptoService.cs | 45 +++++--- 8 files changed, 173 insertions(+), 105 deletions(-) delete mode 100644 AdvancedSystems.Security.Abstractions/IRSACryptoService.cs create mode 100644 AdvancedSystems.Security.Abstractions/RSACryptoContract.cs diff --git a/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs b/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs deleted file mode 100644 index 276a2ac..0000000 --- a/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; - -namespace AdvancedSystems.Security.Abstractions; - -/// -/// Represents a contract for performing RSA-based asymmetric operations. -/// -public interface IRSACryptoService : IDisposable -{ - #region Properties - - X509Certificate2 Certificate { get; } - - HashFunction HashFunction { get; } - - RSAEncryptionPadding EncryptionPadding { get; } - - RSASignaturePadding SignaturePadding { get; } - - #endregion - - #region Methods - - /// - /// Encrypts the input . - /// - /// - /// The data to encrypt. - /// - /// - /// The encrypted data. - /// - byte[] Encrypt(byte[] data); - - /// - /// Decrypts the . - /// - /// - /// The data to decrypt. - /// - /// - /// The decrypted data. - /// - byte[] Decrypt(byte[] cipher); - - byte[] SignData(byte[] data); - - bool VerifyData(byte[] data, byte[] signature); - - #endregion -} \ No newline at end of file diff --git a/AdvancedSystems.Security.Abstractions/RSACryptoContract.cs b/AdvancedSystems.Security.Abstractions/RSACryptoContract.cs new file mode 100644 index 0000000..45d0cea --- /dev/null +++ b/AdvancedSystems.Security.Abstractions/RSACryptoContract.cs @@ -0,0 +1,104 @@ +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace AdvancedSystems.Security.Abstractions; + +/// +/// Represents a contract for performing RSA-based asymmetric operations. +/// +public abstract class RSACryptoContract +{ + #region Properties + + /// + public abstract X509Certificate2 Certificate { get; } + + /// + public abstract HashFunction HashFunction { get; set; } + + /// + public abstract RSAEncryptionPadding EncryptionPadding { get; set; } + + /// + public abstract RSASignaturePadding SignaturePadding { get; set; } + + #endregion + + #region Methods + + /// + /// Encrypts the input . + /// + /// + /// The data to encrypt. + /// + /// + /// The encrypted data. + /// + /// + /// Raised if this object has already been disposed. + /// + /// + /// Raised if the public key of the specified certificate is null. + /// + public abstract byte[] Encrypt(byte[] data); + + /// + /// Decrypts the . + /// + /// + /// The data to decrypt. + /// + /// + /// The decrypted data. + /// + /// + /// Raised if this object has already been disposed. + /// + /// + /// Raised if the private key of the specified certificate is null. + /// + public abstract byte[] Decrypt(byte[] cipher); + + /// + /// Computes the hash value of the specified data and signs it. + /// + /// + /// The input data to hash and sign. + /// + /// + /// The RSA signature for the specified data. + /// + /// + /// Raised if this object has already been disposed. + /// + /// + /// Raised if the private key of the specified certificate is null. + /// + public abstract byte[] SignData(byte[] data); + + /// + /// Verifies that a digital signature is valid by calculating the + /// hash value of the specified data using the specified hash algorithm + /// and padding, and comparing it to the provided signature. + /// + /// + /// The signed data. + /// + /// + /// The signature data to be verified. + /// + /// + /// if the signature is valid; otherwise, . + /// + /// + /// Raised if this object has already been disposed. + /// + /// + /// Raised if the public key of the specified certificate is null. + /// + public abstract bool VerifyData(byte[] data, byte[] signature); + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs index 7bbdd78..2d2548e 100644 --- a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs @@ -24,7 +24,7 @@ public RSACryptoServiceTests(HostFixture certificateFixture) #region Tests /// - /// Tests that encrypts an array of bytes correctly + /// Tests that encrypts an array of bytes correctly /// by using a pre-configured certificate. /// [Fact] diff --git a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs index 6c86336..106b40c 100644 --- a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs +++ b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs @@ -10,7 +10,7 @@ namespace AdvancedSystems.Security.Cryptography; /// /// Represents a class for performing RSA-based asymmetric operations. /// -public sealed class RSACryptoProvider : IDisposable +public sealed class RSACryptoProvider : RSACryptoContract, IDisposable { private bool _isDisposed = false; @@ -19,24 +19,19 @@ public RSACryptoProvider(X509Certificate2 certificate) this.Certificate = certificate; } - ~RSACryptoProvider() - { - this.Dispose(false); - } - #region Properties - public X509Certificate2 Certificate { get; private set; } + public override X509Certificate2 Certificate { get; } - public HashFunction HashFunction { get; set; } = HashFunction.SHA256; + public override HashFunction HashFunction { get; set; } = HashFunction.SHA256; - public RSAEncryptionPadding EncryptionPadding { get; set; } = RSAEncryptionPadding.OaepSHA256; + public override RSAEncryptionPadding EncryptionPadding { get; set; } = RSAEncryptionPadding.OaepSHA256; - public RSASignaturePadding SignaturePadding { get; set; } = RSASignaturePadding.Pss; + public override RSASignaturePadding SignaturePadding { get; set; } = RSASignaturePadding.Pss; #endregion - #region Public Methods + #region Methods public void Dispose() { @@ -56,46 +51,59 @@ private void Dispose(bool disposing) this._isDisposed = true; } - /// - public byte[] Encrypt(byte[] data) + /// + public override byte[] Encrypt(byte[] data) { - using RSA? publicKey = this.Certificate.GetRSAPublicKey(); - ArgumentNullException.ThrowIfNull(publicKey, nameof(publicKey)); + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + + using RSA publicKey = this.Certificate.GetRSAPublicKey() + ?? throw new CryptographicException("Public Key is null."); byte[] cipher = publicKey.Encrypt(data, this.EncryptionPadding); return cipher; } - /// - public byte[] Decrypt(byte[] cipher) + /// + public override byte[] Decrypt(byte[] cipher) { + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + if (!this.Certificate.HasPrivateKey) { - throw new CryptographicException($"Certificate with thumbprint '{this.Certificate.Thumbprint}' has no private key."); + throw new CryptographicException($"Certificate with thumbprint \"{this.Certificate.Thumbprint}\" has no private key."); } - using RSA? privateKey = this.Certificate.GetRSAPrivateKey(); - ArgumentNullException.ThrowIfNull(privateKey, nameof(privateKey)); + using RSA privateKey = this.Certificate.GetRSAPrivateKey() + ?? throw new CryptographicException("Private Key is null."); byte[] source = privateKey.Decrypt(cipher, this.EncryptionPadding); return source; } - /// - public byte[] SignData(byte[] data) + /// + public override byte[] SignData(byte[] data) { - using RSA? privateKey = this.Certificate.GetRSAPrivateKey(); - ArgumentNullException.ThrowIfNull(privateKey, nameof(privateKey)); + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + + if (!this.Certificate.HasPrivateKey) + { + throw new CryptographicException($"Certificate with thumbprint \"{this.Certificate.Thumbprint}\" has no private key."); + } + + using RSA privateKey = this.Certificate.GetRSAPrivateKey() + ?? throw new CryptographicException("Private Key is null."); byte[] signature = privateKey.SignData(data, this.HashFunction.ToHashAlgorithmName(), this.SignaturePadding); return signature; } - /// - public bool VerifyData(byte[] data, byte[] signature) + /// + public override bool VerifyData(byte[] data, byte[] signature) { - using RSA? publicKey = this.Certificate.GetRSAPublicKey(); - ArgumentNullException.ThrowIfNull(publicKey, nameof(publicKey)); + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + + using RSA publicKey = this.Certificate.GetRSAPublicKey() + ?? throw new CryptographicException("Public Key is null."); bool isVerified = publicKey.VerifyData(data, signature, this.HashFunction.ToHashAlgorithmName(), this.SignaturePadding); return isVerified; diff --git a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs index ed22c98..7687d31 100644 --- a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs @@ -177,13 +177,13 @@ public static IServiceCollection AddKDFService(this IServiceCollection services) private static IServiceCollection AddRSACryptoService(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Transient()); return services; } /// - /// Adds the default implementation of to . + /// Adds the default implementation of to . /// /// /// The service collection containing the service. @@ -202,7 +202,7 @@ public static IServiceCollection AddRSACryptoService(this IServiceCollection ser } /// - /// Adds the default implementation of to . + /// Adds the default implementation of to . /// /// /// The service collection containing the service. diff --git a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs index c0dc448..04c43a6 100644 --- a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs +++ b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs @@ -11,7 +11,7 @@ namespace AdvancedSystems.Security.Extensions; /// Defines functions for interacting with X.509 certificates. /// /// -public static partial class CertificateExtensions +public static class CertificateExtensions { /// /// Attempts to parse the specified distinguished name (DN) string into a object. diff --git a/AdvancedSystems.Security/Options/RSACryptoOptions.cs b/AdvancedSystems.Security/Options/RSACryptoOptions.cs index 396923d..eb7c9a1 100644 --- a/AdvancedSystems.Security/Options/RSACryptoOptions.cs +++ b/AdvancedSystems.Security/Options/RSACryptoOptions.cs @@ -6,7 +6,7 @@ namespace AdvancedSystems.Security.Options; /// -/// Configures options for the . +/// Configures options for the . /// public sealed record RSACryptoOptions { diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs index cf477b0..85f3091 100644 --- a/AdvancedSystems.Security/Services/RSACryptoService.cs +++ b/AdvancedSystems.Security/Services/RSACryptoService.cs @@ -14,7 +14,7 @@ namespace AdvancedSystems.Security.Services; /// /// Represents a service for performing RSA-based asymmetric operations. /// -public sealed class RSACryptoService : IRSACryptoService +public sealed class RSACryptoService : RSACryptoContract, IDisposable { private bool _isDisposed = false; private readonly ILogger _logger; @@ -28,47 +28,56 @@ public RSACryptoService(ILogger logger, ICertificateService ce this.Certificate = certificateService.GetCertificate("default", rsaOptions1.Thumbprint, validOnly: true) ?? throw new ArgumentNullException(nameof(rsaOptions)); - this._provider = new RSACryptoProvider(this.Certificate); - this._provider.HashFunction = rsaOptions1.HashFunction; - this._provider.EncryptionPadding = rsaOptions1.EncryptionPadding; - this._provider.SignaturePadding = rsaOptions1.SignaturePadding; - } - - ~RSACryptoService() - { - this.Dispose(false); + this._provider = new RSACryptoProvider(this.Certificate) + { + HashFunction = rsaOptions1.HashFunction, + EncryptionPadding = rsaOptions1.EncryptionPadding, + SignaturePadding = rsaOptions1.SignaturePadding + }; } #region Properties /// - public X509Certificate2 Certificate { get; } + public override X509Certificate2 Certificate { get; } /// - public HashFunction HashFunction + public override HashFunction HashFunction { get { return this._provider.HashFunction; } + set + { + this._provider.HashFunction = value; + } } /// - public RSAEncryptionPadding EncryptionPadding + public override RSAEncryptionPadding EncryptionPadding { get { return this._provider.EncryptionPadding; } + set + { + this._provider.EncryptionPadding = value; + } } /// - public RSASignaturePadding SignaturePadding + public override RSASignaturePadding SignaturePadding { get { return this._provider.SignaturePadding; } + set + { + this._provider.SignaturePadding = value; + } } #endregion @@ -96,25 +105,25 @@ private void Dispose(bool disposing) } /// - public byte[] Encrypt(byte[] buffer) + public override byte[] Encrypt(byte[] buffer) { return this._provider.Encrypt(buffer); } /// - public byte[] Decrypt(byte[] cipher) + public override byte[] Decrypt(byte[] cipher) { return this._provider.Decrypt(cipher); } /// - public byte[] SignData(byte[] data) + public override byte[] SignData(byte[] data) { return this._provider.SignData(data); } /// - public bool VerifyData(byte[] data, byte[] signature) + public override bool VerifyData(byte[] data, byte[] signature) { return this._provider.VerifyData(data, signature); } From 55e518858de265ef38af53351d634a930b89bab1 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 18 Jul 2025 21:14:16 +0200 Subject: [PATCH 09/15] Test RSACryptoServiceTests --- .../Services/RSACryptoServiceTests.cs | 45 ++++++++++++++++--- .../Cryptography/RSACryptoProvider.cs | 4 ++ .../Options/RSACryptoOptions.cs | 10 +++++ .../Services/RSACryptoService.cs | 15 ++++--- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs index 2d2548e..0d7daf5 100644 --- a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs @@ -1,9 +1,18 @@ -using AdvancedSystems.Security.Abstractions; +using System; +using System.Security.Cryptography; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; +using AdvancedSystems.Security.Options; using AdvancedSystems.Security.Services; using AdvancedSystems.Security.Tests.Fixtures; using AdvancedSystems.Security.Tests.Helpers; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using Moq; using Xunit; @@ -15,10 +24,31 @@ namespace AdvancedSystems.Security.Tests.Services; public sealed class RSACryptoServiceTests : IClassFixture { private readonly HostFixture _certificateFixture; + private readonly Mock> _logger = new(); + private readonly RSACryptoService _sut; public RSACryptoServiceTests(HostFixture certificateFixture) { this._certificateFixture = certificateFixture; + + var rsaOptions = new RSACryptoOptions + { + HashFunction = HashFunction.SHA256, + EncryptionPadding = RSAEncryptionPadding.OaepSHA256, + SignaturePadding = RSASignaturePadding.Pss, + Thumbprint = Certificates.PasswordCertificateThumbprint, + StoreService = this._certificateFixture.ConfiguredStoreService, + ValidOnly = false, + }; + + ICertificateService certificateService = this._certificateFixture.Host?.Services.GetService() + ?? throw new InvalidOperationException($"Failed to retrieve {nameof(ICertificateService)} from DI container."); + + this._sut = new RSACryptoService( + this._logger.Object, + certificateService, + Microsoft.Extensions.Options.Options.Create(rsaOptions) + ); } #region Tests @@ -31,17 +61,20 @@ public RSACryptoServiceTests(HostFixture certificateFixture) public void TestEncryptionDecryption_Roundtrip() { // Arrange - string storeService = this._certificateFixture.ConfiguredStoreService; - string thumbprint = Certificates.PasswordCertificateThumbprint; + string message = "Hello, World!"; + byte[] buffer = message.GetBytes(Format.String); // Act - ICertificateService? certificateService = this._certificateFixture.Host?.Services.GetService(); - var certificate = certificateService?.GetCertificate(storeService, thumbprint, validOnly: false); + byte[] cipher = this._sut.Encrypt(buffer); + byte[] source = this._sut.Decrypt(cipher); + string decryptedMessage = source.ToString(Format.String); // Assert Assert.Multiple(() => { - Assert.NotNull(certificateService); + Assert.NotEmpty(cipher); + Assert.NotEmpty(source); + Assert.Equal(message, decryptedMessage); }); } diff --git a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs index 106b40c..de0f853 100644 --- a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs +++ b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs @@ -21,12 +21,16 @@ public RSACryptoProvider(X509Certificate2 certificate) #region Properties + /// public override X509Certificate2 Certificate { get; } + /// public override HashFunction HashFunction { get; set; } = HashFunction.SHA256; + /// public override RSAEncryptionPadding EncryptionPadding { get; set; } = RSAEncryptionPadding.OaepSHA256; + /// public override RSASignaturePadding SignaturePadding { get; set; } = RSASignaturePadding.Pss; #endregion diff --git a/AdvancedSystems.Security/Options/RSACryptoOptions.cs b/AdvancedSystems.Security/Options/RSACryptoOptions.cs index eb7c9a1..de54314 100644 --- a/AdvancedSystems.Security/Options/RSACryptoOptions.cs +++ b/AdvancedSystems.Security/Options/RSACryptoOptions.cs @@ -35,6 +35,16 @@ public sealed record RSACryptoOptions [Required] public required string Thumbprint { get; set; } + /// + /// + /// + public required string StoreService { get; set; } + + /// + /// + /// + public bool ValidOnly { get; set; } = true; + /// /// Set this value to to allow only valid certificates to be /// used for the encryption and decryption; otherwise, . diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs index 85f3091..714ff0b 100644 --- a/AdvancedSystems.Security/Services/RSACryptoService.cs +++ b/AdvancedSystems.Security/Services/RSACryptoService.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Abstractions.Exceptions; using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Options; @@ -20,19 +21,19 @@ public sealed class RSACryptoService : RSACryptoContract, IDisposable private readonly ILogger _logger; private readonly RSACryptoProvider _provider; - public RSACryptoService(ILogger logger, ICertificateService certificateService, IOptions rsaOptions) + public RSACryptoService(ILogger logger, ICertificateService certificateService, IOptions options) { this._logger = logger; - RSACryptoOptions rsaOptions1 = rsaOptions.Value; + RSACryptoOptions rsaOptions = options.Value; - this.Certificate = certificateService.GetCertificate("default", rsaOptions1.Thumbprint, validOnly: true) - ?? throw new ArgumentNullException(nameof(rsaOptions)); + this.Certificate = certificateService.GetCertificate(rsaOptions.StoreService, rsaOptions.Thumbprint, rsaOptions.ValidOnly) + ?? throw new CertificateNotFoundException($"Failed to retrieve certificate with options {nameof(RSACryptoOptions.StoreService)}=\"{rsaOptions.StoreService}\" and {nameof(RSACryptoOptions.Thumbprint)}=\"{rsaOptions.Thumbprint}\"."); this._provider = new RSACryptoProvider(this.Certificate) { - HashFunction = rsaOptions1.HashFunction, - EncryptionPadding = rsaOptions1.EncryptionPadding, - SignaturePadding = rsaOptions1.SignaturePadding + HashFunction = rsaOptions.HashFunction, + EncryptionPadding = rsaOptions.EncryptionPadding, + SignaturePadding = rsaOptions.SignaturePadding }; } From ab29fcdbe6d3ea429dbd2cfc8b1b6186098e91d1 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 18 Jul 2025 21:37:07 +0200 Subject: [PATCH 10/15] Refactor RSACryptoServiceTests --- .../Services/RSACryptoServiceTests.cs | 10 ++++++++-- AdvancedSystems.Security/Options/RSACryptoOptions.cs | 12 +----------- .../Services/RSACryptoService.cs | 10 +++++++++- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs index 0d7daf5..a9058c6 100644 --- a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs @@ -25,6 +25,7 @@ public sealed class RSACryptoServiceTests : IClassFixture { private readonly HostFixture _certificateFixture; private readonly Mock> _logger = new(); + private readonly Mock _certificateService = new(); private readonly RSACryptoService _sut; public RSACryptoServiceTests(HostFixture certificateFixture) @@ -38,17 +39,22 @@ public RSACryptoServiceTests(HostFixture certificateFixture) SignaturePadding = RSASignaturePadding.Pss, Thumbprint = Certificates.PasswordCertificateThumbprint, StoreService = this._certificateFixture.ConfiguredStoreService, - ValidOnly = false, }; ICertificateService certificateService = this._certificateFixture.Host?.Services.GetService() ?? throw new InvalidOperationException($"Failed to retrieve {nameof(ICertificateService)} from DI container."); + // NOTE: Use invalid certificates for testing purposes only + this._certificateService.Setup(x => x.GetCertificate(rsaOptions.StoreService, rsaOptions.Thumbprint, true)) + .Returns(certificateService.GetCertificate(rsaOptions.StoreService, rsaOptions.Thumbprint, false)); + this._sut = new RSACryptoService( this._logger.Object, - certificateService, + this._certificateService.Object, Microsoft.Extensions.Options.Options.Create(rsaOptions) ); + + this._certificateService.VerifyAll(); } #region Tests diff --git a/AdvancedSystems.Security/Options/RSACryptoOptions.cs b/AdvancedSystems.Security/Options/RSACryptoOptions.cs index de54314..6f7c985 100644 --- a/AdvancedSystems.Security/Options/RSACryptoOptions.cs +++ b/AdvancedSystems.Security/Options/RSACryptoOptions.cs @@ -38,16 +38,6 @@ public sealed record RSACryptoOptions /// /// /// + [Required] public required string StoreService { get; set; } - - /// - /// - /// - public bool ValidOnly { get; set; } = true; - - /// - /// Set this value to to allow only valid certificates to be - /// used for the encryption and decryption; otherwise, . - /// - public bool RequireValidCertificate { get; set; } = true; } \ No newline at end of file diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs index 714ff0b..4807791 100644 --- a/AdvancedSystems.Security/Services/RSACryptoService.cs +++ b/AdvancedSystems.Security/Services/RSACryptoService.cs @@ -26,7 +26,7 @@ public RSACryptoService(ILogger logger, ICertificateService ce this._logger = logger; RSACryptoOptions rsaOptions = options.Value; - this.Certificate = certificateService.GetCertificate(rsaOptions.StoreService, rsaOptions.Thumbprint, rsaOptions.ValidOnly) + this.Certificate = certificateService.GetCertificate(rsaOptions.StoreService, rsaOptions.Thumbprint, validOnly: true) ?? throw new CertificateNotFoundException($"Failed to retrieve certificate with options {nameof(RSACryptoOptions.StoreService)}=\"{rsaOptions.StoreService}\" and {nameof(RSACryptoOptions.Thumbprint)}=\"{rsaOptions.Thumbprint}\"."); this._provider = new RSACryptoProvider(this.Certificate) @@ -108,24 +108,32 @@ private void Dispose(bool disposing) /// public override byte[] Encrypt(byte[] buffer) { + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + return this._provider.Encrypt(buffer); } /// public override byte[] Decrypt(byte[] cipher) { + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + return this._provider.Decrypt(cipher); } /// public override byte[] SignData(byte[] data) { + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + return this._provider.SignData(data); } /// public override bool VerifyData(byte[] data, byte[] signature) { + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + return this._provider.VerifyData(data, signature); } From d21677df34d9cc713ff8781faec3c88c20ecbf21 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Fri, 18 Jul 2025 21:56:52 +0200 Subject: [PATCH 11/15] Remove logger from RSACryptoService --- .../Services/RSACryptoServiceTests.cs | 3 --- AdvancedSystems.Security/Services/RSACryptoService.cs | 5 +---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs index a9058c6..5afeb6c 100644 --- a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs @@ -10,7 +10,6 @@ using AdvancedSystems.Security.Tests.Helpers; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Moq; @@ -24,7 +23,6 @@ namespace AdvancedSystems.Security.Tests.Services; public sealed class RSACryptoServiceTests : IClassFixture { private readonly HostFixture _certificateFixture; - private readonly Mock> _logger = new(); private readonly Mock _certificateService = new(); private readonly RSACryptoService _sut; @@ -49,7 +47,6 @@ public RSACryptoServiceTests(HostFixture certificateFixture) .Returns(certificateService.GetCertificate(rsaOptions.StoreService, rsaOptions.Thumbprint, false)); this._sut = new RSACryptoService( - this._logger.Object, this._certificateService.Object, Microsoft.Extensions.Options.Options.Create(rsaOptions) ); diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs index 4807791..0e4b930 100644 --- a/AdvancedSystems.Security/Services/RSACryptoService.cs +++ b/AdvancedSystems.Security/Services/RSACryptoService.cs @@ -7,7 +7,6 @@ using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Options; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace AdvancedSystems.Security.Services; @@ -18,12 +17,10 @@ namespace AdvancedSystems.Security.Services; public sealed class RSACryptoService : RSACryptoContract, IDisposable { private bool _isDisposed = false; - private readonly ILogger _logger; private readonly RSACryptoProvider _provider; - public RSACryptoService(ILogger logger, ICertificateService certificateService, IOptions options) + public RSACryptoService(ICertificateService certificateService, IOptions options) { - this._logger = logger; RSACryptoOptions rsaOptions = options.Value; this.Certificate = certificateService.GetCertificate(rsaOptions.StoreService, rsaOptions.Thumbprint, validOnly: true) From 7df2bd1c4d604de71ccdae5e476a3ad319ca05d9 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 19 Jul 2025 19:20:45 +0200 Subject: [PATCH 12/15] Start migrating signatures from arrays to spans --- .../IHashService.cs | 2 +- .../IKDFService.cs | 6 +++-- .../RSACryptoContract.cs | 8 +++--- .../Cryptography/HMACTests.cs | 8 +++--- .../Cryptography/HashTests.cs | 23 ++++++++-------- .../Cryptography/RSACryptoProviderTests.cs | 27 +++++++------------ .../Extensions/CoreExtensionsTests.cs | 15 ++++++----- .../Services/HMACServiceTests.cs | 6 ++--- .../Services/HashServiceTests.cs | 2 +- .../Services/KDFServiceTests.cs | 10 ++++--- .../Services/RSACryptoServiceTests.cs | 13 +++------ .../Cryptography/HashProvider.cs | 4 +-- .../Cryptography/RSACryptoProvider.cs | 23 ++++++++-------- .../Extensions/CoreExtensions.cs | 4 +-- .../Services/CertificateService.cs | 2 +- .../Services/HashService.cs | 6 +++-- .../Services/KDFService.cs | 7 ++--- .../Services/RSACryptoService.cs | 26 +++++++++--------- 18 files changed, 95 insertions(+), 97 deletions(-) diff --git a/AdvancedSystems.Security.Abstractions/IHashService.cs b/AdvancedSystems.Security.Abstractions/IHashService.cs index cab1229..d1b87fd 100644 --- a/AdvancedSystems.Security.Abstractions/IHashService.cs +++ b/AdvancedSystems.Security.Abstractions/IHashService.cs @@ -28,7 +28,7 @@ public interface IHashService /// /// Raised if the specified is not implemented. /// - byte[] Compute(HashFunction hashFunction, byte[] buffer); + Span Compute(HashFunction hashFunction, Span buffer); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Abstractions/IKDFService.cs b/AdvancedSystems.Security.Abstractions/IKDFService.cs index 0804187..a566d73 100644 --- a/AdvancedSystems.Security.Abstractions/IKDFService.cs +++ b/AdvancedSystems.Security.Abstractions/IKDFService.cs @@ -1,4 +1,6 @@ -namespace AdvancedSystems.Security.Abstractions; +using System; + +namespace AdvancedSystems.Security.Abstractions; /// /// Represents a contract employing for key derivation functions. @@ -51,7 +53,7 @@ public interface IKDFService /// /// 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, Span password, Span salt, int hashSize, int iterations, out byte[]? pbkdf2); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Abstractions/RSACryptoContract.cs b/AdvancedSystems.Security.Abstractions/RSACryptoContract.cs index 45d0cea..56b2f3b 100644 --- a/AdvancedSystems.Security.Abstractions/RSACryptoContract.cs +++ b/AdvancedSystems.Security.Abstractions/RSACryptoContract.cs @@ -42,7 +42,7 @@ public abstract class RSACryptoContract /// /// Raised if the public key of the specified certificate is null. /// - public abstract byte[] Encrypt(byte[] data); + public abstract Span Encrypt(Span data); /// /// Decrypts the . @@ -59,7 +59,7 @@ public abstract class RSACryptoContract /// /// Raised if the private key of the specified certificate is null. /// - public abstract byte[] Decrypt(byte[] cipher); + public abstract Span Decrypt(Span cipher); /// /// Computes the hash value of the specified data and signs it. @@ -76,7 +76,7 @@ public abstract class RSACryptoContract /// /// Raised if the private key of the specified certificate is null. /// - public abstract byte[] SignData(byte[] data); + public abstract Span SignData(Span data); /// /// Verifies that a digital signature is valid by calculating the @@ -98,7 +98,7 @@ public abstract class RSACryptoContract /// /// Raised if the public key of the specified certificate is null. /// - public abstract bool VerifyData(byte[] data, byte[] signature); + public abstract bool VerifyData(Span data, Span signature); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs index 5fd78c9..37d98f3 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs @@ -44,11 +44,11 @@ public sealed class HMACTests public void TestHMAC_Value(HashFunction hashFunction, string text, string expectedMac) { // Arrange - byte[] key = "secret".GetBytes(Format.String); - byte[] buffer = text.GetBytes(Format.String); + Span key = "secret".GetBytes(Format.String); + Span buffer = text.GetBytes(Format.String); // Act - byte[] actualMac = HMACProvider.Compute(hashFunction, key, buffer); + Span actualMac = HMACProvider.Compute(hashFunction, key, buffer); // Assert Assert.Equal(expectedMac.GetBytes(Format.Hex), actualMac); @@ -79,7 +79,7 @@ public void TestHMAC_Size(HashFunction hashFunction, string text) { // Arrange int keySize = 32; - byte[] buffer = text.GetBytes(Format.String); + Span buffer = text.GetBytes(Format.String); int expectedMacSize = hashFunction.GetSize(); // Act diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs index 292508e..2874968 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System; +using System.Security.Cryptography; using System.Text; using AdvancedSystems.Security.Abstractions; @@ -37,10 +38,10 @@ public void TestMD5Hash(string input, string expected, Format format) { // Arrange Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); + Span buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(HashFunction.MD5, buffer); + Span hash = HashProvider.Compute(HashFunction.MD5, buffer); string md5 = hash.ToString(format); // Assert @@ -68,10 +69,10 @@ public void TestSHA1Hash(string input, string expected, Format format) { // Arrange Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); + Span buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(HashFunction.SHA1, buffer); + Span hash = HashProvider.Compute(HashFunction.SHA1, buffer); string sha1 = hash.ToString(format); // Assert @@ -99,10 +100,10 @@ public void TestSHA256Hash(string input, string expected, Format format) { // Arrange Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); + Span buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(HashFunction.SHA256, buffer); + Span hash = HashProvider.Compute(HashFunction.SHA256, buffer); string sha256 = hash.ToString(format); // Assert @@ -130,10 +131,10 @@ public void TestSHA384Hash(string input, string expected, Format format) { // Arrange Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); + Span buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(HashFunction.SHA384, buffer); + Span hash = HashProvider.Compute(HashFunction.SHA384, buffer); string sha384 = hash.ToString(format); // Assert @@ -161,10 +162,10 @@ public void TestSHA512Hash(string input, string expected, Format format) { // Arrange Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); + Span buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(HashFunction.SHA512, buffer); + Span hash = HashProvider.Compute(HashFunction.SHA512, buffer); string sha512 = hash.ToString(format); // Assert diff --git a/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs b/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs index ddc1e5c..2a247af 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs @@ -1,4 +1,6 @@ -using AdvancedSystems.Security.Cryptography; +using System; + +using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Extensions; using AdvancedSystems.Security.Tests.Fixtures; @@ -29,20 +31,15 @@ public void TestEncryptionDecryption_Roundtrip() { // Arrange string message = "Hello, World!"; - byte[] buffer = message.GetBytes(Format.String); + Span buffer = message.GetBytes(Format.String); // Act - byte[] cipher = this._sut.RSACryptoProvider.Encrypt(buffer); - byte[] source = this._sut.RSACryptoProvider.Decrypt(cipher); + Span cipher = this._sut.RSACryptoProvider.Encrypt(buffer); + Span source = this._sut.RSACryptoProvider.Decrypt(cipher); string decryptedMessage = source.ToString(Format.String); // Assert - Assert.Multiple(() => - { - Assert.NotEmpty(cipher); - Assert.NotEmpty(source); - Assert.Equal(message, decryptedMessage); - }); + Assert.Equal(message, decryptedMessage); } /// @@ -54,18 +51,14 @@ public void TestSigningVerification_Roundtrip() { // Arrange string message = "Hello, World!"; - byte[] buffer = message.GetBytes(Format.String); + Span buffer = message.GetBytes(Format.String); // Act - byte[] signature = this._sut.RSACryptoProvider.SignData(buffer); + Span signature = this._sut.RSACryptoProvider.SignData(buffer); bool verified = this._sut.RSACryptoProvider.VerifyData(buffer, signature); // Assert - Assert.Multiple((() => - { - Assert.NotEmpty(signature); - Assert.True(verified); - })); + Assert.True(verified); } #endregion diff --git a/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs b/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs index d3905f0..b7178da 100644 --- a/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs @@ -1,4 +1,5 @@ -using System.Text; +using System; +using System.Text; using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Extensions; @@ -18,11 +19,11 @@ public sealed class CoreExtensionsTests public void TestStringFormatting(string input) { // Arrange - byte[] buffer = input.GetBytes(Format.String); + Span buffer = input.GetBytes(Format.String); // Act string fromBytes = buffer.ToString(Format.String); - byte[] @string = fromBytes.GetBytes(Format.String); + Span @string = fromBytes.GetBytes(Format.String); // Assert Assert.Equal(buffer, @string); @@ -35,11 +36,11 @@ public void TestStringFormatting(string input) public void TestBase64Formatting(int size) { // Arrange - byte[] buffer = CryptoRandomProvider.GetBytes(size).ToArray(); + Span buffer = CryptoRandomProvider.GetBytes(size).ToArray(); // Act string base64 = buffer.ToString(Format.Base64); - byte[] fromBytes = base64.GetBytes(Format.Base64); + Span fromBytes = base64.GetBytes(Format.Base64); // Assert Assert.Equal(buffer, fromBytes); @@ -52,11 +53,11 @@ public void TestBase64Formatting(int size) public void TestHexFormatting(int size) { // Arrange - byte[] buffer = CryptoRandomProvider.GetBytes(size).ToArray(); + Span buffer = CryptoRandomProvider.GetBytes(size).ToArray(); // Act string hexadecimal = buffer.ToString(Format.Hex); - byte[] fromBytes = hexadecimal.GetBytes(Format.Hex); + Span fromBytes = hexadecimal.GetBytes(Format.Hex); // Assert Assert.Equal(buffer, fromBytes); diff --git a/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs index 17c1c74..37c2bc5 100644 --- a/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs @@ -35,13 +35,13 @@ public void TestCompute() // Arrange var sha256 = HashFunction.SHA256; Span key = this._cryptoRandomService.GetBytes(32); - byte[] data = "Hello, World".GetBytes(Format.String); + Span data = "Hello, World".GetBytes(Format.String); // Act - byte[] mac = this._sut.Compute(sha256, key, data); + Span mac = this._sut.Compute(sha256, key, data); // Assert - Assert.NotEmpty(mac); + Assert.NotEmpty(mac.ToArray()); } #endregion diff --git a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs index 3984d09..156f033 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 . /// /// diff --git a/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs b/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs index 2683601..c630e82 100644 --- a/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs @@ -1,4 +1,6 @@ -using AdvancedSystems.Security.Abstractions; +using System; + +using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Extensions; using AdvancedSystems.Security.Tests.Fixtures; @@ -22,7 +24,7 @@ public KDFServiceTests(KDFServiceFixture kdfServiceFixture) #region Tests /// - /// Tests that + /// Tests that /// returns a non-empty hash with success state . /// [Fact] @@ -32,8 +34,8 @@ public void TestTryComputePBKDF2() var sha256 = HashFunction.SHA256; int iterations = 30_000; int saltSize = 128; - byte[] password = "REDACTED".GetBytes(Format.String); - byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); + Span password = "REDACTED".GetBytes(Format.String); + Span salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); // Act bool success = this._sut.TryComputePBKDF2(sha256, password, salt, sha256.GetSize(), iterations, out byte[]? pbkdf2); diff --git a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs index 5afeb6c..8cdcfcd 100644 --- a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs @@ -65,20 +65,15 @@ public void TestEncryptionDecryption_Roundtrip() { // Arrange string message = "Hello, World!"; - byte[] buffer = message.GetBytes(Format.String); + Span buffer = message.GetBytes(Format.String); // Act - byte[] cipher = this._sut.Encrypt(buffer); - byte[] source = this._sut.Decrypt(cipher); + Span cipher = this._sut.Encrypt(buffer); + Span source = this._sut.Decrypt(cipher); string decryptedMessage = source.ToString(Format.String); // Assert - Assert.Multiple(() => - { - Assert.NotEmpty(cipher); - Assert.NotEmpty(source); - Assert.Equal(message, decryptedMessage); - }); + Assert.Multiple(() => Assert.Equal(message, decryptedMessage)); } #endregion diff --git a/AdvancedSystems.Security/Cryptography/HashProvider.cs b/AdvancedSystems.Security/Cryptography/HashProvider.cs index a96fa9b..b88638d 100644 --- a/AdvancedSystems.Security/Cryptography/HashProvider.cs +++ b/AdvancedSystems.Security/Cryptography/HashProvider.cs @@ -10,8 +10,8 @@ namespace AdvancedSystems.Security.Cryptography; /// public static class HashProvider { - /// - public static byte[] Compute(HashFunction hashFunction, byte[] buffer) + /// + public static Span Compute(HashFunction hashFunction, Span buffer) { return hashFunction switch { diff --git a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs index de0f853..a4cf147 100644 --- a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs +++ b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs @@ -37,6 +37,7 @@ public RSACryptoProvider(X509Certificate2 certificate) #region Methods + /// public void Dispose() { this.Dispose(true); @@ -55,20 +56,20 @@ private void Dispose(bool disposing) this._isDisposed = true; } - /// - public override byte[] Encrypt(byte[] data) + /// + public override Span Encrypt(Span data) { ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); using RSA publicKey = this.Certificate.GetRSAPublicKey() ?? throw new CryptographicException("Public Key is null."); - byte[] cipher = publicKey.Encrypt(data, this.EncryptionPadding); + Span cipher = publicKey.Encrypt(data, this.EncryptionPadding); return cipher; } - /// - public override byte[] Decrypt(byte[] cipher) + /// + public override Span Decrypt(Span cipher) { ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); @@ -80,12 +81,12 @@ public override byte[] Decrypt(byte[] cipher) using RSA privateKey = this.Certificate.GetRSAPrivateKey() ?? throw new CryptographicException("Private Key is null."); - byte[] source = privateKey.Decrypt(cipher, this.EncryptionPadding); + Span source = privateKey.Decrypt(cipher, this.EncryptionPadding); return source; } - /// - public override byte[] SignData(byte[] data) + /// + public override Span SignData(Span data) { ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); @@ -97,12 +98,12 @@ public override byte[] SignData(byte[] data) using RSA privateKey = this.Certificate.GetRSAPrivateKey() ?? throw new CryptographicException("Private Key is null."); - byte[] signature = privateKey.SignData(data, this.HashFunction.ToHashAlgorithmName(), this.SignaturePadding); + Span signature = privateKey.SignData(data, this.HashFunction.ToHashAlgorithmName(), this.SignaturePadding); return signature; } - /// - public override bool VerifyData(byte[] data, byte[] signature) + /// + public override bool VerifyData(Span data, Span signature) { ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); diff --git a/AdvancedSystems.Security/Extensions/CoreExtensions.cs b/AdvancedSystems.Security/Extensions/CoreExtensions.cs index 86327d8..e8cb9de 100644 --- a/AdvancedSystems.Security/Extensions/CoreExtensions.cs +++ b/AdvancedSystems.Security/Extensions/CoreExtensions.cs @@ -11,7 +11,7 @@ namespace AdvancedSystems.Security.Extensions; /// public static class CoreExtensions { - public static string ToString(this byte[] array, Format format) + public static string ToString(this Span array, Format format) { return format switch { @@ -22,7 +22,7 @@ public static string ToString(this byte[] array, Format format) }; } - public static byte[] GetBytes(this string @string, Format format) + public static Span GetBytes(this string @string, Format format) { return format switch { diff --git a/AdvancedSystems.Security/Services/CertificateService.cs b/AdvancedSystems.Security/Services/CertificateService.cs index e570f05..73651af 100644 --- a/AdvancedSystems.Security/Services/CertificateService.cs +++ b/AdvancedSystems.Security/Services/CertificateService.cs @@ -160,7 +160,7 @@ public IEnumerable GetCertificate(string storeService) } catch (ArgumentNullException) { - return Enumerable.Empty(); + return []; } finally { diff --git a/AdvancedSystems.Security/Services/HashService.cs b/AdvancedSystems.Security/Services/HashService.cs index 52e35f8..c468478 100644 --- a/AdvancedSystems.Security/Services/HashService.cs +++ b/AdvancedSystems.Security/Services/HashService.cs @@ -1,4 +1,6 @@ -using AdvancedSystems.Security.Abstractions; +using System; + +using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Extensions; @@ -21,7 +23,7 @@ public HashService(ILogger logger) #region Methods /// - public byte[] Compute(HashFunction hashFunction, byte[] buffer) + public Span Compute(HashFunction hashFunction, Span buffer) { if (hashFunction is HashFunction.MD5 or HashFunction.SHA1) { diff --git a/AdvancedSystems.Security/Services/KDFService.cs b/AdvancedSystems.Security/Services/KDFService.cs index e445215..05d77d5 100644 --- a/AdvancedSystems.Security/Services/KDFService.cs +++ b/AdvancedSystems.Security/Services/KDFService.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Diagnostics.CodeAnalysis; using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; @@ -13,9 +14,9 @@ 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) + public bool TryComputePBKDF2(HashFunction hashFunction, Span password, Span salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2) { - return KDFProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out pbkdf2); + return KDFProvider.TryComputePBKDF2(hashFunction, password.ToArray(), salt.ToArray(), hashSize, iterations, out pbkdf2); } #endregion diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs index 0e4b930..8ce38c2 100644 --- a/AdvancedSystems.Security/Services/RSACryptoService.cs +++ b/AdvancedSystems.Security/Services/RSACryptoService.cs @@ -36,10 +36,10 @@ public RSACryptoService(ICertificateService certificateService, IOptions + /// public override X509Certificate2 Certificate { get; } - /// + /// public override HashFunction HashFunction { get @@ -52,7 +52,7 @@ public override HashFunction HashFunction } } - /// + /// public override RSAEncryptionPadding EncryptionPadding { get @@ -65,7 +65,7 @@ public override RSAEncryptionPadding EncryptionPadding } } - /// + /// public override RSASignaturePadding SignaturePadding { get @@ -82,7 +82,7 @@ public override RSASignaturePadding SignaturePadding #region Methods - /// + /// public void Dispose() { this.Dispose(true); @@ -102,32 +102,32 @@ private void Dispose(bool disposing) this._isDisposed = true; } - /// - public override byte[] Encrypt(byte[] buffer) + /// + public override Span Encrypt(Span buffer) { ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); return this._provider.Encrypt(buffer); } - /// - public override byte[] Decrypt(byte[] cipher) + /// + public override Span Decrypt(Span cipher) { ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); return this._provider.Decrypt(cipher); } - /// - public override byte[] SignData(byte[] data) + /// + public override Span SignData(Span data) { ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); return this._provider.SignData(data); } - /// - public override bool VerifyData(byte[] data, byte[] signature) + /// + public override bool VerifyData(Span data, Span signature) { ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); From 9cc18ca2f7d36765395b89a11da5d290a5d14e74 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 19 Jul 2025 19:24:30 +0200 Subject: [PATCH 13/15] Complete base test for RSACryptoService --- .../Cryptography/RSACryptoProviderTests.cs | 4 ++- .../Services/RSACryptoServiceTests.cs | 26 ++++++++++++++++--- .../Cryptography/KDFProvider.cs | 2 +- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs b/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs index 2a247af..af846ef 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs @@ -1,5 +1,6 @@ using System; +using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Extensions; using AdvancedSystems.Security.Tests.Fixtures; @@ -9,7 +10,8 @@ namespace AdvancedSystems.Security.Tests.Cryptography; /// -/// Tests the public methods in . +/// Tests the default implementation of +/// as a provider class (). /// public sealed class RSACryptoProviderTests : IClassFixture { diff --git a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs index 8cdcfcd..d2e7fac 100644 --- a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs @@ -18,7 +18,8 @@ namespace AdvancedSystems.Security.Tests.Services; /// -/// Tests the public methods in . +/// Tests the default implementation of +/// as a service class (). /// public sealed class RSACryptoServiceTests : IClassFixture { @@ -57,7 +58,7 @@ public RSACryptoServiceTests(HostFixture certificateFixture) #region Tests /// - /// Tests that encrypts an array of bytes correctly + /// Tests that encrypts an array of bytes correctly /// by using a pre-configured certificate. /// [Fact] @@ -73,7 +74,26 @@ public void TestEncryptionDecryption_Roundtrip() string decryptedMessage = source.ToString(Format.String); // Assert - Assert.Multiple(() => Assert.Equal(message, decryptedMessage)); + Assert.Equal(message, decryptedMessage); + } + + /// + /// Tests that signs and verifies an array of bytes + /// correctly by using a pre-configured certificate. + /// + [Fact] + public void TestSigningVerification_Roundtrip() + { + // Arrange + string message = "Hello, World!"; + Span buffer = message.GetBytes(Format.String); + + // Act + Span signature = this._sut.SignData(buffer); + bool verified = this._sut.VerifyData(buffer, signature); + + // Assert + Assert.True(verified); } #endregion diff --git a/AdvancedSystems.Security/Cryptography/KDFProvider.cs b/AdvancedSystems.Security/Cryptography/KDFProvider.cs index 41dbc5f..c809bfa 100644 --- a/AdvancedSystems.Security/Cryptography/KDFProvider.cs +++ b/AdvancedSystems.Security/Cryptography/KDFProvider.cs @@ -12,7 +12,7 @@ namespace AdvancedSystems.Security.Cryptography; /// public static class KDFProvider { - /// + /// public static bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2) { try From f8fc74bc4c306ec07e5febb1a49cd5c3593a905b Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 19 Jul 2025 19:29:51 +0200 Subject: [PATCH 14/15] Update dependencies --- .../AdvancedSystems.Security.Tests.csproj | 10 +++++----- .../AdvancedSystems.Security.csproj | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj b/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj index c652eeb..f13a7c1 100644 --- a/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj +++ b/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj @@ -10,15 +10,15 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -26,7 +26,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/AdvancedSystems.Security/AdvancedSystems.Security.csproj b/AdvancedSystems.Security/AdvancedSystems.Security.csproj index 5c765a1..857521f 100644 --- a/AdvancedSystems.Security/AdvancedSystems.Security.csproj +++ b/AdvancedSystems.Security/AdvancedSystems.Security.csproj @@ -13,12 +13,12 @@ - + - - + + From f01ad8503ddbc9d050bb2e18316ae67bedebc9b7 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 19 Jul 2025 22:23:34 +0200 Subject: [PATCH 15/15] Refactor unit tests (dependency injection) --- .../ServiceCollectionExtensionsTests.cs | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index 3332569..8e2fd81 100644 --- a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -1,8 +1,10 @@ -using System.Security.Cryptography.X509Certificates; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.DependencyInjection; +using AdvancedSystems.Security.Options; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; @@ -24,23 +26,20 @@ public sealed class ServiceCollectionExtensionsTests /// /// Tests that can be initialized through dependency injection. /// + /// + /// Although of little practical value, this test verifies that + /// can be registered without having a strong dependency on during + /// the initialization phase. + /// [Fact] public async Task TestAddCertificateService_FromOptions() { // Arrange - string storeService = "my/CurrentUser"; - 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(); }) .Configure(app => @@ -62,7 +61,8 @@ public async Task TestAddCertificateService_FromOptions() #region AddCertificateStore 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 TestAddCertificateStore_FromOptions() @@ -96,24 +96,34 @@ public async Task TestAddCertificateStore_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 TestAddCertificateStore_FromAppSettings() { // Arrange - string storeService = "my/CurrentUser"; + string section = Sections.CERTIFICATE_STORE; + string storeService = "MyStoreService"; + string storeLocation = "CurrentUser"; + string storeName = "My"; + + var appSettings = new Dictionary + { + { $"{section}:{nameof(CertificateStoreOptions.Location)}", storeLocation }, + { $"{section}:{nameof(CertificateStoreOptions.Name)}", storeName }, + }; + + var configurationRoot = new ConfigurationBuilder() + .AddInMemoryCollection(appSettings) + .Build(); using var hostBuilder = 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); + var storeSettings = configurationRoot.GetRequiredSection(section); services.AddCertificateStore(storeService, storeSettings); }) .Configure(app =>