diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml
index 5a7e4e5..eb0ed7b 100644
--- a/.github/workflows/dotnet-tests.yml
+++ b/.github/workflows/dotnet-tests.yml
@@ -1,7 +1,4 @@
-# This workflow will build a .NET project
-# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
-
-name: .NET
+name: "Unit Tests"
on:
push:
diff --git a/AdvancedSystems.Security.Abstractions/Exceptions/CertificateNotFoundException.cs b/AdvancedSystems.Security.Abstractions/Exceptions/CertificateNotFoundException.cs
new file mode 100644
index 0000000..3ecf24a
--- /dev/null
+++ b/AdvancedSystems.Security.Abstractions/Exceptions/CertificateNotFoundException.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace AdvancedSystems.Security.Abstractions.Exceptions
+{
+ ///
+ /// Represents errors that occur because a specified certificate could not be located.
+ ///
+ ///
+ /// This exception is typically thrown when an attempt to retrieve a certificate by its
+ /// identifier, such as a thumbprint or subject name, fails. It indicates that the required
+ /// certificate is not present in the specified certificate store or location.
+ ///
+ public class CertificateNotFoundException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CertificateNotFoundException()
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error .
+ ///
+ /// The error message that explains the reason for the exception.
+ public CertificateNotFoundException(string message) : base(message)
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error
+ /// a reference to the exception that is the cause of this exception.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified.
+ public CertificateNotFoundException(string message, Exception inner) : base(message, inner)
+ {
+
+ }
+ }
+}
diff --git a/AdvancedSystems.Security.Abstractions/ICertificateService.cs b/AdvancedSystems.Security.Abstractions/ICertificateService.cs
index 96aabde..8356af5 100644
--- a/AdvancedSystems.Security.Abstractions/ICertificateService.cs
+++ b/AdvancedSystems.Security.Abstractions/ICertificateService.cs
@@ -1,13 +1,30 @@
-using System.Security.Cryptography.X509Certificates;
+using AdvancedSystems.Security.Abstractions.Exceptions;
+
+using System.Security.Cryptography.X509Certificates;
namespace AdvancedSystems.Security.Abstractions
{
+ ///
+ /// Defines a service for managing and retrieving X.509 certificates.
+ ///
public interface ICertificateService
{
#region Methods
- X509Certificate2? GetStoreCertificate(StoreName storeName, StoreLocation storeLocation, string thumbprint);
+ ///
+ /// Retrieves an X.509 certificate from the specified store using the provided .
+ ///
+ /// The thumbprint of the certificate to locate.
+ /// The certificate store from which to retrieve the certificate.
+ /// The location of the certificate store, such as or .
+ /// The object if the certificate is found, else null.
+ /// Thrown when no certificate with the specified thumbprint is found in the store.
+ X509Certificate2? GetStoreCertificate(string thumbprint, StoreName storeName, StoreLocation storeLocation);
+ ///
+ /// Retrieves an application-configured X.509 certificate.
+ ///
+ /// The object if the certificate is found, else null.
X509Certificate2? GetConfiguredCertificate();
#endregion
diff --git a/AdvancedSystems.Security.Abstractions/ICertificateStore.cs b/AdvancedSystems.Security.Abstractions/ICertificateStore.cs
new file mode 100644
index 0000000..4627ecc
--- /dev/null
+++ b/AdvancedSystems.Security.Abstractions/ICertificateStore.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Security;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+namespace AdvancedSystems.Security.Abstractions
+{
+ ///
+ /// Represents an X.509 store, which is a physical store where certificates are persisted and managed.
+ ///
+ public interface ICertificateStore : IDisposable
+ {
+ #region Properties
+
+ ///
+ /// Gets an handle to an HCERTSTORE store.
+ ///
+ IntPtr StoreHandle { get; }
+
+ ///
+ /// Gets the location of the X.509 certificate store.
+ ///
+ StoreLocation Location { get; }
+
+ ///
+ /// Gets the name of the X.509 certificate store.
+ ///
+ string? Name { get; }
+
+ ///
+ /// Returns a collection of certificates located in an X.509 certificate store.
+ ///
+ X509Certificate2Collection Certificates { get; }
+
+ ///
+ /// Gets a value that indicates whether the instance is connected to an open certificate store.
+ ///
+ bool IsOpen { get; }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Opens an X.509 certificate store or creates a new store, depending on flag settings.
+ ///
+ /// A bitwise combination of enumeration values that specifies the way to open the X.509 certificate store.
+ /// The store cannot be opened as requested.
+ /// The caller does not have the required permission.
+ /// The store contains invalid values.
+ ///
+ /// Use this method to open an existing X.509 store. Note that you must have additional permissions, specified by
+ /// StorePermissionFlags, to enumerate the certificates in the store. You can create a new store
+ /// by passing a store name that does not exist to the class constructor, and then using any of the
+ /// flags except .
+ ///
+ void Open(OpenFlags flags);
+
+ ///
+ /// Closes an X.509 certificate store.
+ ///
+ ///
+ /// This method releases all resources associated with the store. You should always
+ /// close an X.509 certificate store after use.
+ ///
+ void Close();
+
+ ///
+ /// Adds a certificate to an X.509 certificate store.
+ ///
+ /// The certificate to add.
+ /// is null.
+ /// The certificate could not be added to the store.
+ void Add(X509Certificate2 certificate);
+
+ ///
+ /// Adds a collection of certificates to an X.509 certificate store.
+ ///
+ /// The collection of certificates to add.
+ /// is null.
+ /// The caller does not have the required permission.
+ ///
+ /// This method adds more than one certificate to an X.509 certificate store; if one certificate
+ /// addition fails, the operation is reverted and no certificates are added.
+ ///
+ void AddRange(X509Certificate2Collection certificates);
+
+ ///
+ /// Removes a certificate from an X.509 certificate store.
+ ///
+ /// The certificate to remove.
+ /// is null.
+ /// The caller does not have the required permission.
+ void Remove(X509Certificate2 certificate);
+
+ ///
+ /// Removes a range of certificates from an X.509 certificate store.
+ ///
+ /// A range of certificates to remove.
+ /// is null.
+ /// The caller does not have the required permission.
+ ///
+ /// This method removes more than one certificate from an X.509 certificate store; if one certificate
+ /// removal fails, the operation is reverted and no certificates are removed.
+ ///
+ void RemoveRange(X509Certificate2Collection certificates);
+
+ #endregion
+ }
+}
diff --git a/AdvancedSystems.Security.Abstractions/IHashService.cs b/AdvancedSystems.Security.Abstractions/IHashService.cs
index 1736b48..a2aa84d 100644
--- a/AdvancedSystems.Security.Abstractions/IHashService.cs
+++ b/AdvancedSystems.Security.Abstractions/IHashService.cs
@@ -3,7 +3,7 @@
namespace AdvancedSystems.Security.Abstractions
{
///
- /// Defines a methods for computing hash codes.
+ /// Defines a service for computing hash codes.
///
public interface IHashService
{
diff --git a/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj b/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj
index 7d746a6..8a8f61e 100644
--- a/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj
+++ b/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj
@@ -14,18 +14,19 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -36,4 +37,10 @@
+
+
+ PreserveNewest
+
+
+
diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs
index 0449199..699b0b5 100644
--- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs
+++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs
@@ -10,6 +10,8 @@ namespace AdvancedSystems.Security.Tests.Cryptography;
public class HashTests
{
+ #region Tests
+
[Theory]
[InlineData("Hello, World!", "65a8e27d8879283831b664bd8b7f0ad4", Format.Hex)]
[InlineData("Hello, World!", "ZajifYh5KDgxtmS9i38K1A==", Format.Base64)]
@@ -104,4 +106,6 @@ public void TestSHA512Hash(string input, string expected, Format format)
// Assert
Assert.Equal(expected, sha512);
}
+
+ #endregion
}
diff --git a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs
new file mode 100644
index 0000000..75389b0
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Options;
+using AdvancedSystems.Security.Services;
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+using Moq;
+
+namespace AdvancedSystems.Security.Tests.Fixtures;
+
+public class CertificateFixture
+{
+ public CertificateFixture()
+ {
+ this.Logger = new Mock>();
+ this.Options = new Mock>();
+ this.Store = new Mock();
+ this.CertificateService = new CertificateService(this.Logger.Object, this.Options.Object, this.Store.Object);
+ }
+
+ #region Properties
+
+ public Mock> Logger { get; private set; }
+
+ public ICertificateService CertificateService { get; private set; }
+
+ public Mock> Options { get; private set; }
+
+ public Mock Store { get; private set; }
+
+ #endregion
+
+ #region Helper Methods
+
+ public static X509Certificate2 CreateCertificate(string subjectName)
+ {
+ using var ecdsa = ECDsa.Create();
+ var request = new CertificateRequest(subjectName, ecdsa, HashAlgorithmName.SHA256);
+ var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddHours(1));
+ return certificate;
+ }
+
+ public static X509Certificate2Collection CreateCertificateCollection(int length)
+ {
+ var certificates = Enumerable.Range(0, length)
+ .Select(_ => CreateCertificate("O=AdvancedSystems"))
+ .ToArray();
+
+ return new X509Certificate2Collection(certificates);
+ }
+
+ #endregion
+}
diff --git a/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs
new file mode 100644
index 0000000..0a53936
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs
@@ -0,0 +1,19 @@
+using System.Security.Cryptography.X509Certificates;
+
+using AdvancedSystems.Security.Services;
+
+namespace AdvancedSystems.Security.Tests.Fixtures;
+
+public class CertificateStoreFixture
+{
+ public CertificateStoreFixture()
+ {
+ this.CertificateStore = new CertificateStore(StoreName.My, StoreLocation.CurrentUser);
+ }
+
+ #region Properties
+
+ public CertificateStore CertificateStore { get; set; }
+
+ #endregion
+}
diff --git a/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs
index bf9ac75..f2cdb82 100644
--- a/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs
+++ b/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs
@@ -9,13 +9,17 @@ namespace AdvancedSystems.Security.Tests.Fixtures;
public class HashServiceFixture
{
- public IHashService HashService { get; set; }
-
- public Mock> Logger { get; set; }
-
public HashServiceFixture()
{
this.Logger = new Mock>();
this.HashService = new HashService(this.Logger.Object);
}
+
+ #region Properties
+
+ public Mock> Logger { get; private set; }
+
+ public IHashService HashService { get; private set; }
+
+ #endregion
}
diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs
new file mode 100644
index 0000000..21c1536
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs
@@ -0,0 +1,192 @@
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.DependencyInjection;
+using AdvancedSystems.Security.Options;
+using AdvancedSystems.Security.Tests.Fixtures;
+
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+using Moq;
+
+using Xunit;
+
+namespace AdvancedSystems.Security.Tests.Services;
+
+public class CertificateServiceTests : IClassFixture
+{
+ private readonly CertificateFixture _sut;
+
+ public CertificateServiceTests(CertificateFixture fixture)
+ {
+ this._sut = fixture;
+ }
+
+ #region Tests
+
+ [Fact]
+ public void TestGetStoreCertificate()
+ {
+ // Arrange
+ var certificates = CertificateFixture.CreateCertificateCollection(3);
+ string thumbprint = certificates.Select(x => x.Thumbprint).First();
+ this._sut.Store.Setup(x => x.Certificates)
+ .Returns(certificates);
+
+ // Act
+ var certificate = this._sut.CertificateService.GetStoreCertificate(thumbprint, StoreName.My, StoreLocation.CurrentUser);
+
+ // Assert
+ Assert.NotNull(certificate);
+ Assert.Equal(thumbprint, certificate.Thumbprint);
+ this._sut.Store.Verify(service => service.Open(It.Is(flags => flags == OpenFlags.ReadOnly)));
+ }
+
+ [Fact]
+ public void TestGetStoreCertificate_NotFound()
+ {
+ // Arrange
+ string thumbprint = "123456789";
+ var storeName = StoreName.My;
+ var storeLocation = StoreLocation.CurrentUser;
+ this._sut.Store.Setup(x => x.Certificates)
+ .Returns(new X509Certificate2Collection());
+
+ // Act
+ var certificate = this._sut.CertificateService.GetStoreCertificate(thumbprint, storeName, storeLocation);
+
+ // Assert
+ Assert.Null(certificate);
+ }
+
+ [Fact]
+ public void GetConfiguredCertificate()
+ {
+ // Arrange
+ var certificates = CertificateFixture.CreateCertificateCollection(3);
+ var certificateOptions = new CertificateOptions
+ {
+ Thumbprint = certificates.Select(x => x.Thumbprint).First(),
+ Store = new CertificateStoreOptions
+ {
+ Location = StoreLocation.CurrentUser,
+ Name = StoreName.My,
+ }
+ };
+
+ this._sut.Options.Setup(x => x.Value)
+ .Returns(certificateOptions);
+
+ this._sut.Store.Setup(x => x.Certificates)
+ .Returns(certificates);
+
+ // Act
+ var certificate = this._sut.CertificateService.GetConfiguredCertificate();
+
+ // Assert
+ Assert.NotNull(certificate);
+ Assert.Equal(certificateOptions.Thumbprint, certificate.Thumbprint);
+ this._sut.Store.Verify(service => service.Open(It.Is(flags => flags == OpenFlags.ReadOnly)));
+ }
+
+ [Fact]
+ public void GetConfiguredCertificate_NotFound()
+ {
+ // Arrange
+ var certificateOptions = new CertificateOptions
+ {
+ Thumbprint = "123456789",
+ Store = new CertificateStoreOptions
+ {
+ Location = StoreLocation.CurrentUser,
+ Name = StoreName.My,
+ }
+ };
+
+ this._sut.Options.Setup(x => x.Value)
+ .Returns(certificateOptions);
+
+ this._sut.Store.Setup(x => x.Certificates)
+ .Returns(new X509Certificate2Collection());
+
+ // Act
+ var certificate = this._sut.CertificateService.GetConfiguredCertificate();
+
+ // Assert
+ Assert.Null(certificate);
+ }
+
+ [Fact]
+ public async Task TestAddCertificateService_FromOptions()
+ {
+ // Arrange
+ using var hostBuilder = await new HostBuilder()
+ .ConfigureWebHost(builder => builder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.AddCertificateService(options =>
+ {
+ options.Thumbprint = "123456789";
+ options.Store = new CertificateStoreOptions
+ {
+ Location = StoreLocation.CurrentUser,
+ Name = StoreName.My,
+ };
+ });
+ })
+ .Configure(app =>
+ {
+
+ }))
+ .StartAsync();
+
+ // Act
+ var certificateService = hostBuilder.Services.GetService();
+ var certificate = certificateService?.GetConfiguredCertificate();
+
+ // Assert
+ Assert.NotNull(certificateService);
+ Assert.Null(certificate);
+ await hostBuilder.StopAsync();
+ }
+
+ [Fact]
+ public async Task TestAddCertificateService_FromAppSettings()
+ {
+ // Arrange
+ using var hostBuilder = await new HostBuilder()
+ .ConfigureWebHost(builder => builder
+ .UseTestServer()
+ .ConfigureAppConfiguration(config =>
+ {
+ config.AddJsonFile("appsettings.json", optional: false);
+ })
+ .ConfigureServices((context, services )=>
+ {
+ services.AddCertificateService(context.Configuration);
+ })
+ .Configure(app =>
+ {
+
+ }))
+ .StartAsync();
+
+ // Act
+ var certificateService = hostBuilder.Services.GetService();
+ var certificate = certificateService?.GetConfiguredCertificate();
+
+ // Assert
+ Assert.NotNull(certificateService);
+ Assert.Null(certificate);
+ await hostBuilder.StopAsync();
+ }
+
+ #endregion
+}
diff --git a/AdvancedSystems.Security.Tests/Services/CertificateStore.cs b/AdvancedSystems.Security.Tests/Services/CertificateStore.cs
new file mode 100644
index 0000000..78106f6
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Services/CertificateStore.cs
@@ -0,0 +1,89 @@
+using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.DependencyInjection;
+using AdvancedSystems.Security.Tests.Fixtures;
+
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+using Xunit;
+
+namespace AdvancedSystems.Security.Tests.Services;
+
+public class CertificateStore : IClassFixture
+{
+ private readonly CertificateStoreFixture _sut;
+
+ public CertificateStore(CertificateStoreFixture fixture)
+ {
+ this._sut = fixture;
+ }
+
+ #region Tests
+
+ [Fact]
+ public async Task TestAddCertificateStore_FromOptions()
+ {
+ // Arrange
+ using var hostBuilder = await new HostBuilder()
+ .ConfigureWebHost(builder => builder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.AddCertificateStore(options =>
+ {
+ options.Name = StoreName.My;
+ options.Location = StoreLocation.CurrentUser;
+ });
+ })
+ .Configure(app =>
+ {
+
+ }))
+ .StartAsync();
+
+ // Act
+ var certificateStore = hostBuilder.Services.GetService();
+
+ // Assert
+ Assert.NotNull(certificateStore);
+ await hostBuilder.StopAsync();
+ }
+
+ [Fact]
+ public async Task TestAddCertificateStore_FromAppSettings()
+ {
+ // Arrange
+ using var hostBuilder = await new HostBuilder()
+ .ConfigureWebHost(builder => builder
+ .UseTestServer()
+ .ConfigureAppConfiguration(config =>
+ {
+ config.AddJsonFile("appsettings.json", optional: false);
+ })
+ .ConfigureServices((context, services) =>
+ {
+
+ services.AddCertificateStore(context.Configuration);
+ })
+ .Configure(app =>
+ {
+
+ }))
+ .StartAsync();
+
+ // Act
+ var certificateStore = hostBuilder.Services.GetService();
+
+ // Assert
+ Assert.NotNull(certificateStore);
+ await hostBuilder.StopAsync();
+ }
+
+ #endregion
+}
diff --git a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs
index 5575fd1..d6b0cc7 100644
--- a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs
+++ b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs
@@ -1,8 +1,15 @@
using System;
using System.Text;
+using System.Threading.Tasks;
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.DependencyInjection;
using AdvancedSystems.Security.Tests.Fixtures;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moq;
@@ -20,6 +27,8 @@ public HashServiceTests(HashServiceFixture fixture)
this._sut = fixture;
}
+ #region Tests
+
[Fact]
public void TestMD5Hash()
{
@@ -107,4 +116,36 @@ public void TestSHA512Hash()
// Assert
Assert.Equal("07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6", sha512);
}
+
+ [Fact]
+ public async Task TestAddHashService()
+ {
+ // Arrange
+ using var hostBuilder = await new HostBuilder()
+ .ConfigureWebHost(builder => builder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.AddHashService();
+ })
+ .Configure(app =>
+ {
+
+ }))
+ .StartAsync();
+
+ string input = "The quick brown fox jumps over the lazy dog";
+ byte[] buffer = Encoding.UTF8.GetBytes(input);
+
+ // Act
+ var hashService = hostBuilder.Services.GetService();
+ string? sha256 = hashService?.GetSHA256Hash(buffer);
+
+ // Assert
+ Assert.NotNull(hashService);
+ Assert.Equal("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", sha256);
+ await hostBuilder.StopAsync();
+ }
+
+ #endregion
}
diff --git a/AdvancedSystems.Security.Tests/appsettings.json b/AdvancedSystems.Security.Tests/appsettings.json
new file mode 100644
index 0000000..bd97412
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Certificate": {
+ "Thumbprint": "123456789",
+ "Store": {
+ "Location": "CurrentUser",
+ "Name": "My"
+ }
+ }
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/AdvancedSystems.Security.csproj b/AdvancedSystems.Security/AdvancedSystems.Security.csproj
index f20d0c5..b693231 100644
--- a/AdvancedSystems.Security/AdvancedSystems.Security.csproj
+++ b/AdvancedSystems.Security/AdvancedSystems.Security.csproj
@@ -6,9 +6,12 @@
+
-
+
+
+
diff --git a/AdvancedSystems.Security/Common/ObjectSerializer.cs b/AdvancedSystems.Security/Common/ObjectSerializer.cs
deleted file mode 100644
index 1c2d48b..0000000
--- a/AdvancedSystems.Security/Common/ObjectSerializer.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System;
-using System.Buffers;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace AdvancedSystems.Security.Common;
-
-public static class ObjectSerializer
-{
- ///
- /// Converts the provided value into a .
- ///
- /// The type of the value to serialize.
- /// The to convert and write.
- /// A representation of the .
- /// There is no compatible for or its serializable members.
- public static ReadOnlySpan Serialize(T value) where T : class, new()
- {
- var buffer = new ArrayBufferWriter();
- using var writer = new Utf8JsonWriter(buffer);
- JsonSerializer.Serialize(writer, value);
- return buffer.WrittenSpan;
- }
-
- ///
- /// Parses the text representing a single JSON value into a .
- ///
- /// The type to deserialize the JSON value into.
- /// JSON text to parse.
- /// A representation of the JSON value.
- /// The JSON is invalid, is not compatible with the JSON, or there is remaining data in the Stream.
- /// There is no compatible for or its serializable members.
- public static T Deserialize(ReadOnlySpan buffer) where T : class, new()
- {
- var payload = new Utf8JsonReader(buffer);
- return JsonSerializer.Deserialize(ref payload)!;
- }
-}
diff --git a/AdvancedSystems.Security/DependencyInjection/CryptoServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/CryptoServiceCollectionExtensions.cs
deleted file mode 100644
index adba274..0000000
--- a/AdvancedSystems.Security/DependencyInjection/CryptoServiceCollectionExtensions.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using AdvancedSystems.Security.Abstractions;
-using AdvancedSystems.Security.Services;
-
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
-
-namespace AdvancedSystems.Security.DependencyInjection;
-
-public static class CryptoServiceCollectionExtensions
-{
- public static IServiceCollection AddHashService(this IServiceCollection services)
- {
- services.TryAdd(ServiceDescriptor.Scoped());
- return services;
- }
-
- public static IServiceCollection AddCertificateService(this IServiceCollection services)
- {
- // TODO: Bind settings, and provide a ICertificateBuilder
- services.AddOptions();
- services.TryAdd(ServiceDescriptor.Scoped());
- return services;
- }
-
- public static IServiceCollection AddRSACryptoService(this IServiceCollection services)
- {
- // Register services required by RSACryptoService
- services.AddCertificateService();
-
- // TODO: Bind settings, and provide a IRSACryptoBuilder
- services.AddOptions();
-
- services.TryAdd(ServiceDescriptor.Singleton());
- return services;
- }
-}
diff --git a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..35542a1
--- /dev/null
+++ b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs
@@ -0,0 +1,115 @@
+using System;
+
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Options;
+using AdvancedSystems.Security.Services;
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace AdvancedSystems.Security.DependencyInjection;
+
+public static class ServiceCollectionExtensions
+{
+ #region HashService
+
+ public static IServiceCollection AddHashService(this IServiceCollection services)
+ {
+ services.TryAdd(ServiceDescriptor.Scoped());
+ return services;
+ }
+
+ #endregion
+
+ #region CertificateStore
+
+ internal static void AddCertificateStore(this IServiceCollection services) where TOptions : class
+ {
+ services.TryAdd(ServiceDescriptor.Singleton(serviceProvider =>
+ {
+ var options = serviceProvider.GetRequiredService>().Value switch
+ {
+ CertificateOptions certificateOptions => new { certificateOptions.Store.Name, certificateOptions.Store.Location },
+ CertificateStoreOptions storeOptions => new { storeOptions.Name, storeOptions.Location },
+ _ => throw new NotImplementedException()
+ };
+
+ return new CertificateStore(options.Name, options.Location);
+ }));
+ }
+
+ public static IServiceCollection AddCertificateStore(this IServiceCollection services, Action setupAction)
+ {
+ services.AddOptions()
+ .Configure(setupAction);
+
+ services.AddCertificateStore();
+ return services;
+ }
+
+ public static IServiceCollection AddCertificateStore(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddOptions()
+ .Bind(configuration.GetRequiredSection(Sections.CERTIFICATE_STORE))
+ .ValidateDataAnnotations()
+ .ValidateOnStart();
+
+ services.AddCertificateStore();
+ return services;
+ }
+
+ #endregion
+
+ #region CertificateService
+
+ internal static void AddCertificateService(this IServiceCollection services)
+ {
+ services.TryAdd(ServiceDescriptor.Scoped());
+ }
+
+ public static IServiceCollection AddCertificateService(this IServiceCollection services, Action setupAction)
+ {
+ services.AddOptions()
+ .Configure(setupAction);
+
+ services.AddCertificateStore();
+ services.AddCertificateService();
+
+ return services;
+ }
+
+ public static IServiceCollection AddCertificateService(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddOptions()
+ .Bind(configuration.GetRequiredSection(Sections.CERTIFICATE))
+ .ValidateDataAnnotations()
+ .ValidateOnStart();
+
+ services.AddCertificateStore(configuration);
+ services.AddCertificateService();
+
+ return services;
+ }
+
+ #endregion
+
+ #region RSACryptoService
+
+ internal static IServiceCollection AddRSACryptoService(this IServiceCollection services)
+ {
+ throw new NotImplementedException();
+ }
+ public static IServiceCollection AddRSACryptoService(this IServiceCollection services, Action setupAction)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static IServiceCollection AddRSACryptoService(this IServiceCollection services, IConfiguration configuration)
+ {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+}
diff --git a/AdvancedSystems.Security/Exceptions/CertificateNotFoundException.cs b/AdvancedSystems.Security/Exceptions/CertificateNotFoundException.cs
deleted file mode 100644
index 3fdcbdb..0000000
--- a/AdvancedSystems.Security/Exceptions/CertificateNotFoundException.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using System;
-
-namespace AdvancedSystems.Security.Exceptions;
-
-///
-/// Represents errors that occur because a specified certificate could not be located.
-///
-///
-/// This exception is typically thrown when an attempt to retrieve a certificate by its
-/// identifier, such as a thumbprint or subject name, fails. It indicates that the required
-/// certificate is not present in the specified certificate store or location.
-///
-public class CertificateNotFoundException : Exception
-{
- ///
- /// Initializes a new instance of the class.
- ///
- public CertificateNotFoundException()
- {
-
- }
-
- ///
- /// Initializes a new instance of the class with a specified error .
- ///
- /// The error message that explains the reason for the exception.
- public CertificateNotFoundException(string message) : base(message)
- {
-
- }
-
- ///
- /// Initializes a new instance of the class with a specified error
- /// a reference to the exception that is the cause of this exception.
- ///
- /// The error message that explains the reason for the exception.
- /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified.
- public CertificateNotFoundException(string message, Exception inner) : base(message, inner)
- {
-
- }
-}
diff --git a/AdvancedSystems.Security/Cryptography/Certificate.cs b/AdvancedSystems.Security/Extensions/CertificateStore.cs
similarity index 53%
rename from AdvancedSystems.Security/Cryptography/Certificate.cs
rename to AdvancedSystems.Security/Extensions/CertificateStore.cs
index 22ca3e7..2b77d6f 100644
--- a/AdvancedSystems.Security/Cryptography/Certificate.cs
+++ b/AdvancedSystems.Security/Extensions/CertificateStore.cs
@@ -1,31 +1,31 @@
using System.Linq;
using System.Security.Cryptography.X509Certificates;
-using AdvancedSystems.Security.Exceptions;
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Abstractions.Exceptions;
-namespace AdvancedSystems.Security.Cryptography;
+namespace AdvancedSystems.Security.Extensions;
///
/// Defines functions for interacting with X.509 certificates.
///
///
-public static class Certificate
+public static class CertificateStore
{
///
- /// Retrieves an X509 certificate from the specified store using the provided thumbprint.
+ /// Retrieves an X.509 certificate from the specified store using the provided thumbprint.
///
- /// The name of the certificate store to search in, such as .
- /// The location of the certificate store, such as or .
+ /// The type of the certificate store, which must implement the interface.
+ /// The certificate store from which to retrieve the certificate.
/// The thumbprint of the certificate to locate.
/// The object if the certificate is found.
- /// Thrown when no valid certificate with the specified thumbprint is found in the store.
- public static X509Certificate2 GetStoreCertificate(StoreName storeName, StoreLocation storeLocation, string thumbprint)
+ /// Thrown when no certificate with the specified thumbprint is found in the store.
+ public static X509Certificate2 GetCertificate(this T store, string thumbprint) where T : ICertificateStore
{
- using var store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
var certificate = store.Certificates
- .Find(X509FindType.FindByThumbprint, thumbprint, validOnly: true)
+ .Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType()
.FirstOrDefault();
diff --git a/AdvancedSystems.Security/Options/CertificateOptions.cs b/AdvancedSystems.Security/Options/CertificateOptions.cs
index 94c38f3..499149d 100644
--- a/AdvancedSystems.Security/Options/CertificateOptions.cs
+++ b/AdvancedSystems.Security/Options/CertificateOptions.cs
@@ -1,16 +1,13 @@
using System.ComponentModel.DataAnnotations;
-using System.Security.Cryptography.X509Certificates;
namespace AdvancedSystems.Security.Options;
-public record CertificateOptions
+public sealed class CertificateOptions
{
[Key]
- public required string Thumbprint { get; init; }
+ [Required(AllowEmptyStrings = false)]
+ public required string Thumbprint { get; set; }
- [EnumDataType(typeof(StoreName))]
- public required StoreName StoreName { get; init; }
-
- [EnumDataType(typeof(StoreLocation))]
- public required StoreLocation StoreLocation { get; init; }
+ [Required]
+ public required CertificateStoreOptions Store { get; set; }
}
diff --git a/AdvancedSystems.Security/Options/CertificateStoreOptions.cs b/AdvancedSystems.Security/Options/CertificateStoreOptions.cs
new file mode 100644
index 0000000..551ab74
--- /dev/null
+++ b/AdvancedSystems.Security/Options/CertificateStoreOptions.cs
@@ -0,0 +1,15 @@
+using System.ComponentModel.DataAnnotations;
+using System.Security.Cryptography.X509Certificates;
+
+namespace AdvancedSystems.Security.Options;
+
+public sealed class CertificateStoreOptions
+{
+ [Required]
+ [EnumDataType(typeof(StoreName))]
+ public required StoreName Name { get; set; }
+
+ [Required]
+ [EnumDataType(typeof(StoreLocation))]
+ public required StoreLocation Location { get; set; }
+}
diff --git a/AdvancedSystems.Security/Options/RSACryptoOptions.cs b/AdvancedSystems.Security/Options/RSACryptoOptions.cs
index a41585c..6119e1b 100644
--- a/AdvancedSystems.Security/Options/RSACryptoOptions.cs
+++ b/AdvancedSystems.Security/Options/RSACryptoOptions.cs
@@ -3,13 +3,13 @@
namespace AdvancedSystems.Security.Options;
-public record RSACryptoOptions
+public sealed class RSACryptoOptions
{
- public required HashAlgorithmName HashAlgorithmName { get; init; }
+ public required HashAlgorithmName HashAlgorithmName { get; set; }
- public required RSAEncryptionPadding EncryptionPadding { get; init; }
+ public required RSAEncryptionPadding EncryptionPadding { get; set; }
- public required RSASignaturePadding SignaturePadding { get; init; }
+ public required RSASignaturePadding SignaturePadding { get; set; }
- public required Encoding Encoding { get; init; }
+ public required Encoding Encoding { get; set; }
}
diff --git a/AdvancedSystems.Security/Options/Sections.cs b/AdvancedSystems.Security/Options/Sections.cs
index 48062a5..8409b23 100644
--- a/AdvancedSystems.Security/Options/Sections.cs
+++ b/AdvancedSystems.Security/Options/Sections.cs
@@ -2,6 +2,8 @@
public readonly record struct Sections
{
+ public const string CERTIFICATE_STORE = "Certificate:Store";
+
public const string CERTIFICATE = "Certificate";
public const string RSA = "RSA";
diff --git a/AdvancedSystems.Security/Services/CertificateService.cs b/AdvancedSystems.Security/Services/CertificateService.cs
index e42ca14..87e83ef 100644
--- a/AdvancedSystems.Security/Services/CertificateService.cs
+++ b/AdvancedSystems.Security/Services/CertificateService.cs
@@ -1,8 +1,8 @@
using System.Security.Cryptography.X509Certificates;
using AdvancedSystems.Security.Abstractions;
-using AdvancedSystems.Security.Cryptography;
-using AdvancedSystems.Security.Exceptions;
+using AdvancedSystems.Security.Abstractions.Exceptions;
+using AdvancedSystems.Security.Extensions;
using AdvancedSystems.Security.Options;
using Microsoft.Extensions.Logging;
@@ -15,24 +15,26 @@ namespace AdvancedSystems.Security.Services;
public sealed class CertificateService : ICertificateService
{
private readonly ILogger _logger;
- private readonly IOptions _options;
+ private readonly IOptions _certificateOptions;
+ private readonly ICertificateStore _certificateStore;
- public CertificateService(ILogger logger, IOptions options)
+ public CertificateService(ILogger logger, IOptions certificateOptions, ICertificateStore certificateStore)
{
this._logger = logger;
- this._options = options;
+ this._certificateOptions = certificateOptions;
+ this._certificateStore = certificateStore;
}
#region Public Methods
- public X509Certificate2? GetStoreCertificate(StoreName storeName, StoreLocation storeLocation, string thumbprint)
+ public X509Certificate2? GetStoreCertificate(string thumbprint, StoreName storeName, StoreLocation storeLocation)
{
try
{
using var _ = this._logger.BeginScope("Searching for {thumbprint} in {storeName} at {storeLocation}", thumbprint, storeName, storeLocation);
- return Certificate.GetStoreCertificate(storeName, storeLocation, thumbprint);
+ return this._certificateStore.GetCertificate(thumbprint);
}
- catch (CertificateNotFoundException exception) when (True(() => this._logger.LogError(exception, "Service failed to retrieve certificate.")))
+ catch (CertificateNotFoundException exception) when (True(() => this._logger.LogError(exception, "{Service} failed to retrieve certificate.", nameof(CertificateService))))
{
return null;
}
@@ -40,8 +42,8 @@ public CertificateService(ILogger logger, IOptions
+/// Implements X.509 store, which is a physical store where certificates are persisted and managed.
+/// This class cannot be inherited.
+///
+public sealed class CertificateStore : ICertificateStore
+{
+ private readonly X509Store _store;
+
+ public CertificateStore(StoreName storeName, StoreLocation storeLocation)
+ {
+ this._store = new X509Store(storeName, storeLocation);
+ }
+
+ #region Properties
+
+ public IntPtr StoreHandle
+ {
+ get
+ {
+ return this._store.StoreHandle;
+ }
+ }
+
+ public StoreLocation Location
+ {
+ get
+ {
+ return this._store.Location;
+ }
+ }
+
+ public string? Name
+ {
+ get
+ {
+ return this._store.Name;
+ }
+ }
+
+ public X509Certificate2Collection Certificates
+ {
+ get
+ {
+ return this._store.Certificates;
+ }
+ }
+
+ public bool IsOpen
+ {
+ get
+ {
+ return this._store.IsOpen;
+ }
+ }
+
+ #endregion
+
+ #region Methods
+
+ public void Open(OpenFlags flags)
+ {
+ this._store.Open(flags);
+ }
+
+ public void Dispose()
+ {
+ this._store?.Dispose();
+ }
+
+ public void Close()
+ {
+ this._store?.Close();
+ }
+
+ public void Add(X509Certificate2 certificate)
+ {
+ this._store.Add(certificate);
+ }
+
+ public void AddRange(X509Certificate2Collection certificates)
+ {
+ this._store.AddRange(certificates);
+ }
+
+ public void Remove(X509Certificate2 certificate)
+ {
+ this._store.Remove(certificate);
+ }
+
+ public void RemoveRange(X509Certificate2Collection certificates)
+ {
+ this._store.RemoveRange(certificates);
+ }
+
+ #endregion
+}
diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs
index cd9bfd6..597a6dd 100644
--- a/AdvancedSystems.Security/Services/RSACryptoService.cs
+++ b/AdvancedSystems.Security/Services/RSACryptoService.cs
@@ -89,13 +89,10 @@ public void Dispose()
public void Dispose(bool disposing)
{
- if (this._disposed) return;
+ if (this._disposed || !disposing) return;
- if (disposing)
- {
- this._certificate.Dispose();
- this._disposed = true;
- }
+ this._certificate.Dispose();
+ this._disposed = true;
}
public bool IsValidMessage(string message, RSAEncryptionPadding? padding, Encoding? encoding = null)
diff --git a/readme.md b/readme.md
index 93444ee..34b5d7c 100644
--- a/readme.md
+++ b/readme.md
@@ -15,7 +15,7 @@ This project uses [stryker](https://stryker-mutator.io/) for mutation testing, w
setup to be installed with
```powershell
-dotnet tool restore
+dotnet tool restore --configfile .\AdvancedSystems.Security\nuget.config
```
Run stryker locally: