diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index c3dece2..e2a98a3 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -15,6 +15,13 @@
"husky"
],
"rollForward": false
+ },
+ "dotnet-certificate-tool": {
+ "version": "2.0.9",
+ "commands": [
+ "certificate-tool"
+ ],
+ "rollForward": false
}
}
}
\ No newline at end of file
diff --git a/.github/workflows/dotnet-publish-abstractions.yml b/.github/workflows/dotnet-publish-abstractions.yml
index 87149a0..9db2996 100644
--- a/.github/workflows/dotnet-publish-abstractions.yml
+++ b/.github/workflows/dotnet-publish-abstractions.yml
@@ -38,16 +38,36 @@ jobs:
shell: powershell
- name: Restore Dependencies
- run: dotnet restore ${{ env.PROJECT_PATH }} --configfile nuget.config
+ run: >
+ dotnet restore ${{ env.PROJECT_PATH }}
+ --configfile nuget.config
- name: Build Project
- run: dotnet build ${{ env.PROJECT_PATH }} --nologo --no-restore --configuration Release
+ run: >
+ dotnet build ${{ env.PROJECT_PATH }}
+ --configuration Release
+ --no-restore
+ --nologo
- name: Pack Project
- run: dotnet pack ${{ env.PROJECT_PATH }} --nologo --no-restore --no-build --configuration Release --output ${{ env.RELEASE_DIRECTORY }}
+ run: >
+ dotnet pack ${{ env.PROJECT_PATH }}
+ --configuration Release
+ --output ${{ env.RELEASE_DIRECTORY }}
+ --no-restore
+ --no-build
+ --nologo
- name: Deploy Package to GitHub
- run: dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg' --skip-duplicate --api-key ${{ secrets.GH_AUTH_TOKEN_PUSH }} --source ${{ env.GITHUB_SOURCE }}
+ run: >
+ dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg'
+ --api-key ${{ secrets.GH_AUTH_TOKEN_PUSH }}
+ --source ${{ env.GITHUB_SOURCE }}
+ --skip-duplicate
- name: Deploy Package to NuGet
- run: dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg' --skip-duplicate --api-key ${{ secrets.NUGET_AUTH_TOKEN_PUSH }} --source ${{ env.NUGET_SOURCE }}
+ run: >
+ dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg'
+ --api-key ${{ secrets.NUGET_AUTH_TOKEN_PUSH }}
+ --source ${{ env.NUGET_SOURCE }}
+ --skip-duplicate
diff --git a/.github/workflows/dotnet-publish-core.yml b/.github/workflows/dotnet-publish-core.yml
index adb9da2..7f4465b 100644
--- a/.github/workflows/dotnet-publish-core.yml
+++ b/.github/workflows/dotnet-publish-core.yml
@@ -38,16 +38,36 @@ jobs:
shell: powershell
- name: Restore Dependencies
- run: dotnet restore ${{ env.PROJECT_PATH }} --configfile nuget.config
+ run: >
+ dotnet restore ${{ env.PROJECT_PATH }}
+ --configfile nuget.config
- name: Build Project
- run: dotnet build ${{ env.PROJECT_PATH }} --nologo --no-restore --configuration Release
+ run: >
+ dotnet build ${{ env.PROJECT_PATH }}
+ --configuration Release
+ --no-restore
+ --nologo
- name: Pack Project
- run: dotnet pack ${{ env.PROJECT_PATH }} --nologo --no-restore --no-build --configuration Release --output ${{ env.RELEASE_DIRECTORY }}
+ run: >
+ dotnet pack ${{ env.PROJECT_PATH }}
+ --configuration Release
+ --output ${{ env.RELEASE_DIRECTORY }}
+ --no-restore
+ --no-build
+ --nologo
- name: Deploy Package to GitHub
- run: dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg' --skip-duplicate --api-key ${{ secrets.GH_AUTH_TOKEN_PUSH }} --source ${{ env.GITHUB_SOURCE }}
+ run: >
+ dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg'
+ --api-key ${{ secrets.GH_AUTH_TOKEN_PUSH }}
+ --source ${{ env.GITHUB_SOURCE }}
+ --skip-duplicate
- name: Deploy Package to NuGet
- run: dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg' --skip-duplicate --api-key ${{ secrets.NUGET_AUTH_TOKEN_PUSH }} --source ${{ env.NUGET_SOURCE }}
+ run: >
+ dotnet nuget push '${{ env.RELEASE_DIRECTORY }}\*.nupkg'
+ --api-key ${{ secrets.NUGET_AUTH_TOKEN_PUSH }}
+ --source ${{ env.NUGET_SOURCE }}
+ --skip-duplicate
diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml
index 1160b99..fe16a76 100644
--- a/.github/workflows/dotnet-tests.yml
+++ b/.github/workflows/dotnet-tests.yml
@@ -28,8 +28,54 @@ jobs:
- name: Restore Dependencies
run: dotnet restore
- - name: Build
- run: dotnet build --no-restore
+ - name: Restore Dotnet Tools
+ run: dotnet tool restore
- - name: Test
- run: dotnet test --no-build --verbosity normal
+ - name: Build Project
+ run: >
+ dotnet build ./AdvancedSystems.Security
+ --configuration Release
+ --no-restore
+ /warnAsError
+
+ - name: Import AdvancedSystems Certificate Authority
+ run: >
+ dotnet certificate-tool add --file ./development/AdvancedSystems-CA.pfx
+ --store-name My
+ --store-location CurrentUser
+ --password '${{ secrets.ADV_CA_SECRET }}'
+
+ - name: Import Password Certificate (Windows)
+ if: runner.os == 'Windows'
+ run: |
+ $AppSettings = Get-Content '.\AdvancedSystems.Security.Tests\appsettings.json' -Raw | ConvertFrom-Json
+ $Name = $AppSettings.CertificateStore.Name
+ $Location = $AppSettings.CertificateStore.Location
+ Import-Certificate -FilePath .\development\AdvancedSystems-PasswordCertificate.pem -CertStoreLocation "Cert:\$Location\$Name"
+ shell: powershell
+
+ - name: Import Password Certificate (Ubuntu)
+ if: runner.os == 'Linux'
+ run: |
+ appSettings='./AdvancedSystems.Security.Tests/appsettings.json'
+ name=$(jq -r '.CertificateStore.Name' $appSettings)
+ location=$(jq -r '.CertificateStore.Location' $appSettings)
+ dotnet certificate-tool add --file ./development/AdvancedSystems-PasswordCertificate.pem --store-name $name --store-location $location
+
+ - name: Import Password Certificate (MacOS)
+ if: runner.os == 'macOS'
+ run: |
+ appSettings='./AdvancedSystems.Security.Tests/appsettings.json'
+ name=$(jq -r '.CertificateStore.Name' $appSettings)
+ location=$(jq -r '.CertificateStore.Location' $appSettings)
+ dotnet certificate-tool add --file ./development/AdvancedSystems-PasswordCertificate.pem --store-name $name --store-location $location
+
+ - name: Configure DotNet User Secrets
+ run: |
+ dotnet user-secrets set CertificatePassword '${{ secrets.ADV_CA_SECRET }}' --project ./AdvancedSystems.Security.Tests
+
+ - name: Run Unit Tests
+ run: >
+ dotnet test ./AdvancedSystems.Security.Tests
+ --configuration Release
+ --verbosity normal
diff --git a/.husky/task-runner.json b/.husky/task-runner.json
index 57978a0..cb037bf 100644
--- a/.husky/task-runner.json
+++ b/.husky/task-runner.json
@@ -23,7 +23,10 @@
"command": "dotnet",
"args": [
"build",
- "/warnaserror"
+ "./AdvancedSystems.Security",
+ "--configuration",
+ "Release",
+ "/warnAsError"
]
},
{
@@ -32,7 +35,11 @@
"command": "dotnet",
"args": [
"test",
- "--nologo"
+ "./AdvancedSystems.Security.Tests",
+ "--configuration",
+ "Release",
+ "--verbosity",
+ "minimal"
]
}
]
diff --git a/AdvancedSystems.Security.Abstractions/HashFunction.cs b/AdvancedSystems.Security.Abstractions/HashFunction.cs
new file mode 100644
index 0000000..89ce318
--- /dev/null
+++ b/AdvancedSystems.Security.Abstractions/HashFunction.cs
@@ -0,0 +1,17 @@
+namespace AdvancedSystems.Security.Abstractions;
+
+///
+/// Identifies a mathematical function that maps a string of arbitrary length
+/// (up to a pre-determined maximum size) to a fixed-length string.
+///
+public enum HashFunction
+{
+ MD5 = 0,
+ SHA1 = 1,
+ SHA256 = 2,
+ SHA384 = 3,
+ SHA512 = 4,
+ SHA3_256 = 5,
+ SHA3_384 = 6,
+ SHA3_512 = 7,
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Abstractions/ICertificateService.cs b/AdvancedSystems.Security.Abstractions/ICertificateService.cs
index b578fa9..84954d4 100644
--- a/AdvancedSystems.Security.Abstractions/ICertificateService.cs
+++ b/AdvancedSystems.Security.Abstractions/ICertificateService.cs
@@ -1,45 +1,172 @@
-using System.Security.Cryptography.X509Certificates;
-
-using AdvancedSystems.Security.Abstractions.Exceptions;
+using System.Collections.Generic;
+using System.Security.Cryptography.X509Certificates;
namespace AdvancedSystems.Security.Abstractions;
///
-/// Defines a service for managing and retrieving X.509 certificates.
+/// Defines a contract for managing and retrieving X.509 certificates.
///
+///
+/// See also: .
+///
+///
public interface ICertificateService
{
#region Methods
///
- /// Retrieves an X.509 certificate from the specified store using the provided
- /// .
+ /// Adds a certificate to a certificate store.
///
- ///
- /// The thumbprint of the certificate to locate.
+ ///
+ /// The name of the keyed service to use.
+ ///
+ ///
+ /// The certificate to add.
+ ///
+ ///
+ /// Returns if the was added
+ /// successfully to the certificate store, else .
+ ///
+ bool AddCertificate(string storeService, X509Certificate2 certificate);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ bool TryImportPemCertificate(string storeService, string certificatePath, string privateKeyPath, out X509Certificate2? certificate);
+
+ ///
+ /// Tries to import a PEM certificate file into a certificate store.
+ ///
+ ///
+ /// The name of the keyed service to use.
+ ///
+ ///
+ /// The file path to the PEM certificate.
+ ///
+ ///
+ /// The file path to the PKCS#8 (encrypted) private key associated with the specified certificate.
+ ///
+ ///
+ /// The password required to decrypt the private key (if specified).
+ ///
+ ///
+ /// An output parameter that will contain the imported instance if the operation succeeds;
+ /// otherwise, it will be .
+ ///
+ ///
+ /// if the certificate was imported to the certificate store successfully, else .
+ ///
+ ///
+ /// See also: .
+ ///
+ bool TryImportPemCertificate(string storeService, string certificatePath, string privateKeyPath, string password, out X509Certificate2? certificate);
+
+ ///
+ ///
+ ///
+ ///
+ ///
///
- ///
- /// 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);
+ ///
+ ///
+ ///
+ bool TryImportPfxCertificate(string storeService, string certificatePath, out X509Certificate2? certificate);
///
- /// Retrieves an application-configured X.509 certificate.
+ /// Tries to import a PFX certificate file into a certificate store.
///
+ ///
+ /// The name of the keyed service to use.
+ ///
+ ///
+ /// The file path to the PFX certificate file that needs to be imported.
+ ///
+ ///
+ /// The password required to access the PFX file's private key.
+ ///
+ ///
+ /// An output parameter that will contain the imported instance if the operation succeeds;
+ /// otherwise, it will be .
+ ///
+ ///
+ /// if the certificate was imported to the certificate store successfully, else .
+ ///
+ ///
+ /// See also: .
+ ///
+ bool TryImportPfxCertificate(string storeService, string certificatePath, string password, out X509Certificate2? certificate);
+
+ ///
+ /// Retrieves all certificates from the certificate store.
+ ///
+ ///
+ /// The name of the keyed service to use.
+ ///
+ ///
+ /// Returns a collection of certificates.
+ ///
+ IEnumerable GetCertificate(string storeService);
+
+ ///
+ /// Retrieves a certificate from the certificate store by using the .
+ ///
+ ///
+ /// The name of the keyed service to use.
+ ///
+ ///
+ /// The string representing the thumbprint of the certificate to retrieve.
+ ///
+ ///
+ /// to allow only valid certificates to be returned from the search;
+ /// otherwise, .
+ ///
+ ///
+ /// A object if a certificate in the certificate store
+ /// matches the search criteria, else .
+ ///
+ X509Certificate2? GetCertificate(string storeService, string thumbprint, bool validOnly = true);
+
+ ///
+ /// Removes a certificate from the certificate store by using the .
+ ///
+ ///
+ /// The name of the keyed service to use.
+ ///
+ ///
+ /// The string representing the thumbprint of the certificate to remove.
+ ///
///
- /// The object if the certificate is found, else null.
+ /// Returns if a certificate with the specified
+ /// was removed from the certificate store, else .
///
- X509Certificate2? GetConfiguredCertificate();
+ bool RemoveCertificate(string storeService, string thumbprint);
#endregion
}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Abstractions/ICertificateStore.cs b/AdvancedSystems.Security.Abstractions/ICertificateStore.cs
index 8200b8d..6f158f9 100644
--- a/AdvancedSystems.Security.Abstractions/ICertificateStore.cs
+++ b/AdvancedSystems.Security.Abstractions/ICertificateStore.cs
@@ -6,8 +6,9 @@
namespace AdvancedSystems.Security.Abstractions;
///
-/// Represents an X.509 store, which is a physical store where certificates are persisted and managed.
+/// Represents a contract for an X.509 store, which is a physical store where certificates are persisted and managed.
///
+///
public interface ICertificateStore : IDisposable
{
#region Properties
@@ -81,7 +82,7 @@ public interface ICertificateStore : IDisposable
/// The certificate to add.
///
///
- /// Thrown when is null.
+ /// Thrown when is .
///
///
/// Thrown when the certificate could not be added to the store.
@@ -95,7 +96,7 @@ public interface ICertificateStore : IDisposable
/// The collection of certificates to add.
///
///
- /// Thrown when is null.
+ /// Thrown when is .
///
///
/// Thrown when the caller does not have the required permission.
@@ -113,7 +114,7 @@ public interface ICertificateStore : IDisposable
/// The certificate to remove.
///
///
- /// Thrown when is null.
+ /// Thrown when is .
///
///
/// Thrown when the caller does not have the required permission.
@@ -127,7 +128,7 @@ public interface ICertificateStore : IDisposable
/// A range of certificates to remove.
///
///
- /// Thrown when is null.
+ /// Thrown when is .
///
///
/// Thrown when the caller does not have the required permission.
diff --git a/AdvancedSystems.Security.Abstractions/ICryptoRandomService.cs b/AdvancedSystems.Security.Abstractions/ICryptoRandomService.cs
index 2de1888..0f95bb9 100644
--- a/AdvancedSystems.Security.Abstractions/ICryptoRandomService.cs
+++ b/AdvancedSystems.Security.Abstractions/ICryptoRandomService.cs
@@ -2,6 +2,9 @@
namespace AdvancedSystems.Security.Abstractions;
+///
+/// Represents a contract for performing cryptographically secure numerical operations.
+///
public interface ICryptoRandomService
{
#region Methods
diff --git a/AdvancedSystems.Security.Abstractions/IHMACService.cs b/AdvancedSystems.Security.Abstractions/IHMACService.cs
new file mode 100644
index 0000000..ba5f8f0
--- /dev/null
+++ b/AdvancedSystems.Security.Abstractions/IHMACService.cs
@@ -0,0 +1,66 @@
+using System;
+
+namespace AdvancedSystems.Security.Abstractions;
+
+///
+/// Represents a contract designed for computing Hash-Based Message Authentication Codes (HMAC).
+///
+///
+public interface IHMACService
+{
+ #region Methods
+
+ ///
+ /// Computes the HMAC of using the specified .
+ ///
+ ///
+ /// The hash function to use.
+ ///
+ ///
+ /// The HMAC key. This cryptographic key uniquely identifies one or more entities.
+ ///
+ ///
+ /// The data to HMAC.
+ ///
+ ///
+ /// The HMAC of the data. This cryptographic checksum is the result of passing through
+ /// through a message authentication algorithm.
+ ///
+ ///
+ /// Raised if an unsupported is passed to this method.
+ ///
+ ///
+ ///
+ /// Message Authentication Codes (MACs) are primarily used for message authentication. MACs based on
+ /// cryptographic hash functions are known as HMACs. Their purpose is to authenticate both the source
+ /// of a message as well as its integrity. A message authentication algorithm is called HMAC, while the
+ /// result of applying HMAC is called the MAC.
+ ///
+ ///
+ /// HMAC keys should be generated from a cryptographically strong random number generator.
+ /// You must securely store the HMAC and treat this information as a secret.
+ /// The value of the secret key should only be known to the message originator and the intended receiver(s).
+ /// The recommended key size is at least 32-bytes for full-entropy keys.
+ ///
+ ///
+ /// The hash function does not require collision-resistance as long as the hash function meets the very
+ /// weak requirement of being almost universal (c.f. ).
+ /// FIPS complieant hash algorithms are defined by FIPS PUB 180-4:
+ ///
+ /// -
+ ///
+ ///
+ /// -
+ ///
+ ///
+ /// -
+ ///
+ ///
+ ///
+ /// See also: .
+ ///
+ ///
+ public byte[] Compute(HashFunction hashFunction, ReadOnlySpan key, ReadOnlySpan buffer);
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Abstractions/IHashService.cs b/AdvancedSystems.Security.Abstractions/IHashService.cs
index 45ae618..cab1229 100644
--- a/AdvancedSystems.Security.Abstractions/IHashService.cs
+++ b/AdvancedSystems.Security.Abstractions/IHashService.cs
@@ -3,73 +3,32 @@
namespace AdvancedSystems.Security.Abstractions;
///
-/// Defines a service for computing hash codes.
+/// Represents a contract designed for computing hash algorithms.
///
+///
public interface IHashService
{
#region Methods
///
- /// Computes the MD5 hash value for the specified byte array.
+ /// Computes the hash value for the specified byte array.
///
- ///
- /// The input to compute the hash code for.
- ///
- ///
- /// The hexadecimal representation of the computed hash code.
- ///
- ///
- [Obsolete("MD5 is not a cryptographically secure hash algorithm.")]
- string GetMD5Hash(byte[] buffer);
-
- ///
- /// Computes the SHA1 hash value for the specified byte array.
- ///
- ///
- /// The input to compute the hash code for.
+ ///
+ /// The hash algorithm implementation to use.
///
- ///
- /// The hexadecimal representation of the computed hash code.
- ///
- ///
- [Obsolete("SHA1 is not a cryptographically secure hash algorithm.")]
- string GetSHA1Hash(byte[] buffer);
-
- ///
- /// Computes the SHA256 hash value for the specified byte array.
- ///
- ///
- /// The input to compute the hash code for.
- ///
- ///
- /// The hexadecimal representation of the computed hash code.
- ///
- ///
- string GetSHA256Hash(byte[] buffer);
-
- ///
- /// Computes the SHA384 hash value for the specified byte array.
- ///
- ///
- /// The input to compute the hash code for.
- ///
- ///
- /// The hexadecimal representation of the computed hash code.
- ///
- ///
- string GetSHA384Hash(byte[] buffer);
-
- ///
- /// Computes the SHA512 hash value for the specified byte array.
- ///
///
/// The input to compute the hash code for.
///
///
- /// The hexadecimal representation of the computed hash code.
+ /// The computed hash code.
///
- ///
- string GetSHA512Hash(byte[] buffer);
+ ///
+ /// WARNING: Do not use this method to compute hashes for confidential data (e.g., passwords).
+ ///
+ ///
+ /// Raised if the specified is not implemented.
+ ///
+ byte[] Compute(HashFunction hashFunction, byte[] buffer);
#endregion
}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Abstractions/IKDFService.cs b/AdvancedSystems.Security.Abstractions/IKDFService.cs
new file mode 100644
index 0000000..0804187
--- /dev/null
+++ b/AdvancedSystems.Security.Abstractions/IKDFService.cs
@@ -0,0 +1,57 @@
+namespace AdvancedSystems.Security.Abstractions;
+
+///
+/// Represents a contract employing for key derivation functions.
+///
+///
+public interface IKDFService
+{
+ #region Methods
+
+ ///
+ /// Attempts to compute a PBKDF2 (password-based key derivation function).
+ /// This method is suitable for securely hashing passwords.
+ ///
+ ///
+ /// The hash algorithm to use to derive the hash.
+ ///
+ ///
+ /// The password used to derive the hash.
+ ///
+ ///
+ /// The salt used to derive the hash.
+ ///
+ ///
+ /// The size of hash to derive.
+ ///
+ ///
+ /// The number of iterations for the operation.
+ ///
+ ///
+ /// A byte array containing the created PBKDF2 derived hash.
+ ///
+ ///
+ /// if the operation succeeds; otherwise, .
+ ///
+ ///
+ /// Supported algorithms for the parameter are:
+ ///
+ /// -
+ ///
+ ///
+ /// -
+ ///
+ ///
+ /// -
+ ///
+ ///
+ /// -
+ ///
+ ///
+ ///
+ /// Additionally, some platforms may support SHA3-equivalent hash functions.
+ ///
+ bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, out byte[]? pbkdf2);
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs b/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs
index 9b435eb..28bda07 100644
--- a/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs
+++ b/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs
@@ -5,6 +5,9 @@
namespace AdvancedSystems.Security.Abstractions;
+///
+/// Represents a contract for performing RSA-based asymmetric operations.
+///
public interface IRSACryptoService : IDisposable
{
#region Properties
@@ -23,15 +26,13 @@ public interface IRSACryptoService : IDisposable
#region Methods
- bool IsValidMessage(string message, RSAEncryptionPadding? padding, Encoding? encoding = null);
+ byte[] Encrypt(byte[] message);
- string Encrypt(string message, Encoding? encoding = null);
+ byte[] Decrypt(byte[] cipher);
- string Decrypt(string cipher, Encoding? encoding = null);
+ byte[] SignData(byte[] data);
- string SignData(string data, Encoding? encoding = null);
-
- bool VerifyData(string data, string signature, Encoding? encoding = null);
+ bool VerifyData(byte[] data, byte[] signature);
#endregion
}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj b/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj
index e4a25e3..c652eeb 100644
--- a/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj
+++ b/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj
@@ -6,29 +6,31 @@
AdvancedSystems.Security.Tests
AdvancedSystems.Security.Tests
true
+ d53eb55d-dc13-4abe-b60b-e11e90d1afbd
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs
new file mode 100644
index 0000000..5fd78c9
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Text;
+
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Cryptography;
+using AdvancedSystems.Security.Extensions;
+
+using Xunit;
+
+namespace AdvancedSystems.Security.Tests.Cryptography;
+
+///
+/// Tests the public methods in .
+///
+public sealed class HMACTests
+{
+ #region Test
+
+ ///
+ /// Tests that
+ /// computes the MAC value of with as cryptographic
+ /// algorithm and a hard-coded key correctly.
+ ///
+ ///
+ /// The hash function to use.
+ ///
+ ///
+ /// The data to HMAC.
+ ///
+ ///
+ /// The expected MAC value for .
+ ///
+ [Theory]
+ [InlineData(HashFunction.MD5, "Hello, World!", "c8972c594d22ce2b4f73e91538db5737")]
+ [InlineData(HashFunction.MD5, "The quick brown fox jumps over the lazy dog", "2e3f3742c21be88e64deb2127fe792d2")]
+ [InlineData(HashFunction.SHA1, "Hello, World!", "883a982dc2ae46d20f7f106c786a9241b60dc340")]
+ [InlineData(HashFunction.SHA1, "The quick brown fox jumps over the lazy dog", "198ea1ea04c435c1246b586a06d5cf11c3ffcda6")]
+ [InlineData(HashFunction.SHA256, "Hello, World!", "fcfaffa7fef86515c7beb6b62d779fa4ccf092f2e61c164376054271252821ff")]
+ [InlineData(HashFunction.SHA256, "The quick brown fox jumps over the lazy dog", "54cd5b827c0ec938fa072a29b177469c843317b095591dc846767aa338bac600")]
+ [InlineData(HashFunction.SHA384, "Hello, World!", "699f60b90c6dc0a32be14690a22c541bc3eba8d3215a782c28d66db3f0a3eb81cc0703c932bc181b02d190c9f9967e41")]
+ [InlineData(HashFunction.SHA384, "The quick brown fox jumps over the lazy dog", "bf8a22d3bd5cf88e0f41fa90eeb00eb908fccd925d55a7305f23e206358bb488fbef01039308e434c255e59f8e3badc3")]
+ [InlineData(HashFunction.SHA512, "Hello, World!", "851caed63934ad1c9a03aef23ba2b84f224bdff4f5148efc57d95f9ae80ca9db2e98bc4c709a529eb1b7234a1ac2e381d28e0eb1efa090bb19613f5c124b6d5b")]
+ [InlineData(HashFunction.SHA512, "The quick brown fox jumps over the lazy dog", "76af3588620ef6e2c244d5a360e080c0d649b6dd6b82ccd115eeefee8ff403bcee9aeb08618db9a2a94a9e80c7996bb2cb0c00f6e69de38ed8af2758ef39df0a")]
+ public void TestHMAC_Value(HashFunction hashFunction, string text, string expectedMac)
+ {
+ // Arrange
+ byte[] key = "secret".GetBytes(Format.String);
+ byte[] buffer = text.GetBytes(Format.String);
+
+ // Act
+ byte[] actualMac = HMACProvider.Compute(hashFunction, key, buffer);
+
+ // Assert
+ Assert.Equal(expectedMac.GetBytes(Format.Hex), actualMac);
+ }
+
+ ///
+ /// Tests that
+ /// the computed MAC equals the size of .
+ ///
+ ///
+ /// The hash function to use.
+ ///
+ ///
+ /// The data to HMAC.
+ ///
+ [Theory]
+ [InlineData(HashFunction.MD5, "Hello, World!")]
+ [InlineData(HashFunction.MD5, "The quick brown fox jumps over the lazy dog")]
+ [InlineData(HashFunction.SHA1, "Hello, World!")]
+ [InlineData(HashFunction.SHA1, "The quick brown fox jumps over the lazy dog")]
+ [InlineData(HashFunction.SHA256, "Hello, World!")]
+ [InlineData(HashFunction.SHA256, "The quick brown fox jumps over the lazy dog")]
+ [InlineData(HashFunction.SHA384, "Hello, World!")]
+ [InlineData(HashFunction.SHA384, "The quick brown fox jumps over the lazy dog")]
+ [InlineData(HashFunction.SHA512, "Hello, World!")]
+ [InlineData(HashFunction.SHA512, "The quick brown fox jumps over the lazy dog")]
+ public void TestHMAC_Size(HashFunction hashFunction, string text)
+ {
+ // Arrange
+ int keySize = 32;
+ byte[] buffer = text.GetBytes(Format.String);
+ int expectedMacSize = hashFunction.GetSize();
+
+ // Act
+ ReadOnlySpan key = CryptoRandomProvider.GetBytes(keySize);
+ byte[] mac = HMACProvider.Compute(hashFunction, key, buffer);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.NotEmpty(mac);
+ Assert.Equal(expectedMacSize, mac.Length * 8);
+ });
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs
index 1fc11c6..292508e 100644
--- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs
+++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs
@@ -1,22 +1,23 @@
using System.Security.Cryptography;
using System.Text;
-using AdvancedSystems.Security.Common;
+using AdvancedSystems.Security.Abstractions;
using AdvancedSystems.Security.Cryptography;
+using AdvancedSystems.Security.Extensions;
using Xunit;
namespace AdvancedSystems.Security.Tests.Cryptography;
///
-/// Tests the public methods in .
+/// Tests the public methods in .
///
public sealed class HashTests
{
#region Tests
///
- /// Tests that the computed hash returns a well-formatted string.
+ /// Tests that the computed hash returns a well-formatted string.
///
///
/// The input to compute the hash code for.
@@ -32,14 +33,14 @@ public sealed class HashTests
[InlineData("Hello, World!", "ZajifYh5KDgxtmS9i38K1A==", Format.Base64)]
[InlineData("The quick brown fox jumps over the lazy dog", "9e107d9d372bb6826bd81d3542a419d6", Format.Hex)]
[InlineData("The quick brown fox jumps over the lazy dog", "nhB9nTcrtoJr2B01QqQZ1g==", Format.Base64)]
- public void TestMd5Hash(string input, string expected, Format format)
+ public void TestMD5Hash(string input, string expected, Format format)
{
// Arrange
Encoding encoding = Encoding.UTF8;
byte[] buffer = encoding.GetBytes(input);
// Act
- byte[] hash = Hash.Compute(buffer, HashAlgorithmName.MD5);
+ byte[] hash = HashProvider.Compute(HashFunction.MD5, buffer);
string md5 = hash.ToString(format);
// Assert
@@ -47,7 +48,7 @@ public void TestMd5Hash(string input, string expected, Format format)
}
///
- /// Tests that the computed hash returns a well-formatted string.
+ /// Tests that the computed hash returns a well-formatted string.
///
///
/// The input to compute the hash code for.
@@ -70,7 +71,7 @@ public void TestSHA1Hash(string input, string expected, Format format)
byte[] buffer = encoding.GetBytes(input);
// Act
- byte[] hash = Hash.Compute(buffer, HashAlgorithmName.SHA1);
+ byte[] hash = HashProvider.Compute(HashFunction.SHA1, buffer);
string sha1 = hash.ToString(format);
// Assert
@@ -78,7 +79,7 @@ public void TestSHA1Hash(string input, string expected, Format format)
}
///
- /// Tests that the computed hash returns a well-formatted string.
+ /// Tests that the computed hash returns a well-formatted string.
///
///
/// The input to compute the hash code for.
@@ -101,7 +102,7 @@ public void TestSHA256Hash(string input, string expected, Format format)
byte[] buffer = encoding.GetBytes(input);
// Act
- byte[] hash = Hash.Compute(buffer, HashAlgorithmName.SHA256);
+ byte[] hash = HashProvider.Compute(HashFunction.SHA256, buffer);
string sha256 = hash.ToString(format);
// Assert
@@ -109,7 +110,7 @@ public void TestSHA256Hash(string input, string expected, Format format)
}
///
- /// Tests that the computed hash returns a well-formatted string.
+ /// Tests that the computed hash returns a well-formatted string.
///
///
/// The input to compute the hash code for.
@@ -132,7 +133,7 @@ public void TestSHA384Hash(string input, string expected, Format format)
byte[] buffer = encoding.GetBytes(input);
// Act
- byte[] hash = Hash.Compute(buffer, HashAlgorithmName.SHA384);
+ byte[] hash = HashProvider.Compute(HashFunction.SHA384, buffer);
string sha384 = hash.ToString(format);
// Assert
@@ -140,7 +141,7 @@ public void TestSHA384Hash(string input, string expected, Format format)
}
///
- /// Tests that the computed hash returns a well-formatted string.
+ /// Tests that the computed hash returns a well-formatted string.
///
///
/// The input to compute the hash code for.
@@ -163,7 +164,7 @@ public void TestSHA512Hash(string input, string expected, Format format)
byte[] buffer = encoding.GetBytes(input);
// Act
- byte[] hash = Hash.Compute(buffer, HashAlgorithmName.SHA512);
+ byte[] hash = HashProvider.Compute(HashFunction.SHA512, buffer);
string sha512 = hash.ToString(format);
// Assert
diff --git a/AdvancedSystems.Security.Tests/Cryptography/KDFProviderTests.cs b/AdvancedSystems.Security.Tests/Cryptography/KDFProviderTests.cs
new file mode 100644
index 0000000..45ce108
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Cryptography/KDFProviderTests.cs
@@ -0,0 +1,91 @@
+using System.Security.Cryptography;
+using System.Text;
+
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Cryptography;
+using AdvancedSystems.Security.Extensions;
+
+using Xunit;
+
+namespace AdvancedSystems.Security.Tests.Cryptography;
+
+public sealed class KDFProviderTests
+{
+ #region Tests
+
+ ///
+ /// Tests that
+ /// computes the hash code successfully and returns the hash with the expected size using the
+ /// algorithm.
+ ///
+ ///
+ /// The specified hash function.
+ ///
+ ///
+ /// The salt size to use.
+ ///
+ [Theory]
+ [InlineData(HashFunction.SHA1, 128)]
+ [InlineData(HashFunction.SHA256, 128)]
+ [InlineData(HashFunction.SHA384, 128)]
+ [InlineData(HashFunction.SHA512, 128)]
+ public void TestTryComputePBKDF2(HashFunction hashFunction, int saltSize)
+ {
+ // Arrange
+ int iterations = 100_000;
+ int hashSize = hashFunction.GetSize();
+
+ byte[] password = Encoding.UTF8.GetBytes("secret");
+ byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray();
+
+ // Act
+ bool isSuccessful = KDFProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out byte[]? pbkdf2);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.True(isSuccessful);
+ Assert.Equal(saltSize, salt.Length);
+ Assert.Equal(hashSize, pbkdf2?.Length);
+ Assert.NotNull(pbkdf2);
+ });
+ }
+
+ ///
+ /// Tests that
+ /// fails to compute the hash code on unsupported values and that the resulting
+ /// hash is empty.
+ ///
+ ///
+ /// The specified hash function.
+ ///
+ ///
+ /// The salt size to use.
+ ///
+ [Theory]
+ [InlineData(HashFunction.SHA3_256, 128)]
+ [InlineData(HashFunction.SHA3_384, 128)]
+ [InlineData(HashFunction.SHA3_512, 128)]
+ public void TestTryComputePBKDF2_HashFunctionSupport(HashFunction hashFunction, int saltSize)
+ {
+ // Arrange
+ int iterations = 100_000;
+ int hashSize = hashFunction.GetSize();
+
+ byte[] password = Encoding.UTF8.GetBytes("secret");
+ byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray();
+
+ // Act
+ bool isSuccessful = KDFProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out byte[]? hash);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ // All current platforms support HMAC-SHA3-256, 384, and 512 together, so we can simplify the check
+ // to just checking HMAC-SHA3-256 for the availability of 384 and 512, too.
+ Assert.Equal(HMACSHA3_256.IsSupported, isSuccessful);
+ });
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs
index f20fcfa..3332569 100644
--- a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs
+++ b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs
@@ -1,10 +1,8 @@
using System.Security.Cryptography.X509Certificates;
-using System.Text;
using System.Threading.Tasks;
using AdvancedSystems.Security.Abstractions;
using AdvancedSystems.Security.DependencyInjection;
-using AdvancedSystems.Security.Options;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
@@ -24,64 +22,26 @@ public sealed class ServiceCollectionExtensionsTests
#region AddCertificateService Tests
///
- /// Tests that can be initialized through dependency injection from configuration options.
+ /// Tests that can be initialized through dependency injection.
///
[Fact]
public async Task TestAddCertificateService_FromOptions()
{
// Arrange
+ string storeService = "my/CurrentUser";
+
using var hostBuilder = await new HostBuilder()
.ConfigureWebHost(builder => builder
.UseTestServer()
.ConfigureServices(services =>
{
- services.AddCertificateService(options =>
+ services.AddCertificateStore(storeService, options =>
{
- options.Thumbprint = "123456789";
- options.Store = new CertificateStoreOptions
- {
- Location = StoreLocation.CurrentUser,
- Name = StoreName.My,
- };
+ options.Location = StoreLocation.CurrentUser;
+ options.Name = StoreName.My;
});
- })
- .Configure(app =>
- {
- }))
- .StartAsync();
-
- // Act
- var certificateService = hostBuilder.Services.GetService();
- var certificate = certificateService?.GetConfiguredCertificate();
-
- // Assert
- Assert.Multiple(() =>
- {
- Assert.NotNull(certificateService);
- Assert.Null(certificate);
- });
-
- await hostBuilder.StopAsync();
- }
-
- ///
- /// Tests that can be initialized through dependency injection from configuration sections.
- ///
- [Fact]
- public async Task TestAddCertificateService_FromAppSettings()
- {
- // Arrange
- using var hostBuilder = await new HostBuilder()
- .ConfigureWebHost(builder => builder
- .UseTestServer()
- .ConfigureAppConfiguration(config =>
- {
- config.AddJsonFile("appsettings.json", optional: false);
- })
- .ConfigureServices((context, services) =>
- {
- services.AddCertificateService(context.Configuration.GetSection(Sections.CERTIFICATE));
+ services.AddCertificateService();
})
.Configure(app =>
{
@@ -91,15 +51,9 @@ public async Task TestAddCertificateService_FromAppSettings()
// Act
var certificateService = hostBuilder.Services.GetService();
- var certificate = certificateService?.GetConfiguredCertificate();
// Assert
- Assert.Multiple(() =>
- {
- Assert.NotNull(certificateService);
- Assert.Null(certificate);
- });
-
+ Assert.NotNull(certificateService);
await hostBuilder.StopAsync();
}
@@ -114,12 +68,14 @@ public async Task TestAddCertificateService_FromAppSettings()
public async Task TestAddCertificateStore_FromOptions()
{
// Arrange
+ string storeService = "my/CurrentUser";
+
using var hostBuilder = await new HostBuilder()
.ConfigureWebHost(builder => builder
.UseTestServer()
.ConfigureServices(services =>
{
- services.AddCertificateStore(options =>
+ services.AddCertificateStore(storeService, options =>
{
options.Name = StoreName.My;
options.Location = StoreLocation.CurrentUser;
@@ -132,7 +88,7 @@ public async Task TestAddCertificateStore_FromOptions()
.StartAsync();
// Act
- var certificateStore = hostBuilder.Services.GetService();
+ var certificateStore = hostBuilder.Services.GetKeyedService(storeService);
// Assert
Assert.NotNull(certificateStore);
@@ -146,6 +102,8 @@ public async Task TestAddCertificateStore_FromOptions()
public async Task TestAddCertificateStore_FromAppSettings()
{
// Arrange
+ string storeService = "my/CurrentUser";
+
using var hostBuilder = await new HostBuilder()
.ConfigureWebHost(builder => builder
.UseTestServer()
@@ -155,7 +113,8 @@ public async Task TestAddCertificateStore_FromAppSettings()
})
.ConfigureServices((context, services) =>
{
- services.AddCertificateStore(context.Configuration.GetSection($"{Sections.CERTIFICATE}:{Sections.STORE}"));
+ var storeSettings = context.Configuration.GetSection(Sections.CERTIFICATE_STORE);
+ services.AddCertificateStore(storeService, storeSettings);
})
.Configure(app =>
{
@@ -164,7 +123,7 @@ public async Task TestAddCertificateStore_FromAppSettings()
.StartAsync();
// Act
- var certificateStore = hostBuilder.Services.GetService();
+ var certificateStore = hostBuilder.Services.GetKeyedService(storeService);
// Assert
Assert.NotNull(certificateStore);
@@ -229,20 +188,77 @@ public async Task TestAddHashService()
}))
.StartAsync();
- string input = "The quick brown fox jumps over the lazy dog";
- byte[] buffer = Encoding.UTF8.GetBytes(input);
-
// Act
var hashService = hostBuilder.Services.GetService();
- string? sha256 = hashService?.GetSHA256Hash(buffer);
// Assert
- Assert.Multiple(() =>
- {
- Assert.NotNull(hashService);
- Assert.Equal("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", sha256);
- });
+ Assert.NotNull(hashService);
+ await hostBuilder.StopAsync();
+ }
+
+ #endregion
+
+ #region AddKDFService Tests
+
+ ///
+ /// Tests that can be initialized through dependency injection.
+ ///
+ [Fact]
+ public async Task TestAddKDFService()
+ {
+ // Arrange
+ using var hostBuilder = await new HostBuilder()
+ .ConfigureWebHost(builder => builder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.AddKDFService();
+ })
+ .Configure(app =>
+ {
+
+ }))
+ .StartAsync();
+
+ // Act
+ var kdfService = hostBuilder.Services.GetService();
+
+ // Assert
+ Assert.NotNull(kdfService);
+ await hostBuilder.StopAsync();
+ }
+
+ #endregion
+
+ #region AddHMACService Tests
+
+ ///
+ /// Tests that can be initialized through dependency injection.
+ ///
+ [Fact]
+ public async Task TestAddHMACService()
+ {
+ // Arrange
+ using var hostBuilder = await new HostBuilder()
+ .ConfigureWebHost(builder =>
+ {
+ builder.UseTestServer();
+ builder.ConfigureServices(services =>
+ {
+ services.AddHMACService();
+ });
+ builder.Configure(app =>
+ {
+
+ });
+ })
+ .StartAsync();
+
+ // Act
+ var cryptoRandomService = hostBuilder.Services.GetService();
+ // Assert
+ Assert.NotNull(cryptoRandomService);
await hostBuilder.StopAsync();
}
diff --git a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs
index 65eacdb..09f42e6 100644
--- a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs
+++ b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs
@@ -1,6 +1,13 @@
-using AdvancedSystems.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Cryptography;
using AdvancedSystems.Security.Extensions;
+using AdvancedSystems.Security.Tests.Fixtures;
+
+using Microsoft.Extensions.DependencyInjection;
+
using Xunit;
namespace AdvancedSystems.Security.Tests.Extensions;
@@ -8,10 +15,50 @@ namespace AdvancedSystems.Security.Tests.Extensions;
///
/// Tests the public methods in .
///
-public sealed class CertificateExtensionsTests
+public sealed class CertificateExtensionsTests : IClassFixture
{
+ private readonly CertificateFixture _certificateFixture;
+
+ public CertificateExtensionsTests(CertificateFixture certificateFixture)
+ {
+ this._certificateFixture = certificateFixture;
+ }
+
#region Tests
+ ///
+ /// Tests that
+ /// parses the DN from a certificate retrieved from a certificate store correctly.
+ ///
+ [Fact]
+ public void TestTryParseDistinguishedName_FromCertificate()
+ {
+ // Arrange
+ string storeService = this._certificateFixture.ConfiguredStoreService;
+ string thumbprint = "2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC";
+
+ // Act
+ ICertificateService? certificateService = this._certificateFixture.Host?.Services.GetService();
+ X509Certificate2? certificate = certificateService?.GetCertificate(storeService, thumbprint, validOnly: false);
+ bool isDsn = CertificateExtensions.TryParseDistinguishedName(certificate?.Subject ?? string.Empty, out DistinguishedName? dn);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.NotNull(certificateService);
+ Assert.NotNull(certificate);
+ Assert.NotNull(certificate?.Subject);
+ Assert.True(isDsn);
+ Assert.NotNull(dn);
+ Assert.Equal("DE", dn?.Country);
+ Assert.Equal("Berlin", dn?.State);
+ Assert.Equal("Berlin", dn?.Locality);
+ Assert.Equal("Advanced Systems", dn?.Organization);
+ Assert.Equal("RnD", dn?.OrganizationalUnit);
+ Assert.Equal("AdvancedSystems-CA", dn?.CommonName);
+ });
+ }
+
///
/// Tests that
/// extracts the RDNs from the DN correctly from the string.
diff --git a/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs b/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs
new file mode 100644
index 0000000..d3905f0
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs
@@ -0,0 +1,66 @@
+using System.Text;
+
+using AdvancedSystems.Security.Cryptography;
+using AdvancedSystems.Security.Extensions;
+
+using Xunit;
+
+namespace AdvancedSystems.Security.Tests.Extensions;
+
+public sealed class CoreExtensionsTests
+{
+ #region Tests
+
+ [Theory]
+ [InlineData("test")]
+ [InlineData("Hello, World!")]
+ [InlineData("The quick brown fox jumps over the lazy dog")]
+ public void TestStringFormatting(string input)
+ {
+ // Arrange
+ byte[] buffer = input.GetBytes(Format.String);
+
+ // Act
+ string fromBytes = buffer.ToString(Format.String);
+ byte[] @string = fromBytes.GetBytes(Format.String);
+
+ // Assert
+ Assert.Equal(buffer, @string);
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(69)]
+ [InlineData(100)]
+ public void TestBase64Formatting(int size)
+ {
+ // Arrange
+ byte[] buffer = CryptoRandomProvider.GetBytes(size).ToArray();
+
+ // Act
+ string base64 = buffer.ToString(Format.Base64);
+ byte[] fromBytes = base64.GetBytes(Format.Base64);
+
+ // Assert
+ Assert.Equal(buffer, fromBytes);
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(69)]
+ [InlineData(100)]
+ public void TestHexFormatting(int size)
+ {
+ // Arrange
+ byte[] buffer = CryptoRandomProvider.GetBytes(size).ToArray();
+
+ // Act
+ string hexadecimal = buffer.ToString(Format.Hex);
+ byte[] fromBytes = hexadecimal.GetBytes(Format.Hex);
+
+ // Assert
+ Assert.Equal(buffer, fromBytes);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs
index 5fe4e6f..a7717d9 100644
--- a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs
+++ b/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs
@@ -1,58 +1,57 @@
-using System;
-using System.Linq;
-using System.Security.Cryptography;
-using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
-using AdvancedSystems.Security.Abstractions;
-using AdvancedSystems.Security.Options;
-using AdvancedSystems.Security.Services;
+using AdvancedSystems.Security.DependencyInjection;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using Moq;
+using Xunit;
namespace AdvancedSystems.Security.Tests.Fixtures;
-public class CertificateFixture
+public sealed class CertificateFixture : IAsyncLifetime
{
- 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 string ConfiguredStoreService { get; } = "my/CurrentUser";
- public Mock Store { get; private set; }
+ public IHost? Host { get; private set; }
#endregion
- #region Helper Methods
+ #region Methods
- public static X509Certificate2 CreateCertificate(string subjectName)
+ public async Task InitializeAsync()
{
- using var ecdsa = ECDsa.Create();
- var request = new CertificateRequest(subjectName, ecdsa, HashAlgorithmName.SHA256);
- var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddHours(1));
- return certificate;
+ this.Host = await new HostBuilder()
+ .ConfigureWebHost(builder => builder
+ .UseTestServer()
+ .ConfigureAppConfiguration(config =>
+ {
+ config.AddJsonFile("appsettings.json", optional: false);
+ })
+ .ConfigureServices((context, services) =>
+ {
+ var storeSettings = context.Configuration.GetSection(Sections.CERTIFICATE_STORE);
+ services.AddCertificateStore(this.ConfiguredStoreService, storeSettings);
+ services.AddCertificateService();
+ })
+ .Configure(app =>
+ {
+
+ }))
+ .StartAsync();
}
- public static X509Certificate2Collection CreateCertificateCollection(int length)
+ public async Task DisposeAsync()
{
- var certificates = Enumerable.Range(0, length)
- .Select(_ => CreateCertificate("O=AdvancedSystems"))
- .ToArray();
+ if (this.Host is null) return;
- return new X509Certificate2Collection(certificates);
+ await this.Host.StopAsync();
}
#endregion
diff --git a/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs
index fbe701d..d64cd4e 100644
--- a/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs
+++ b/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs
@@ -4,7 +4,7 @@
namespace AdvancedSystems.Security.Tests.Fixtures;
-public class CertificateStoreFixture
+public sealed class CertificateStoreFixture
{
public CertificateStoreFixture()
{
diff --git a/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs
index 52c6e6e..9540026 100644
--- a/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs
+++ b/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs
@@ -3,7 +3,7 @@
namespace AdvancedSystems.Security.Tests.Fixtures;
-public class CryptoRandomFixture
+public sealed class CryptoRandomFixture
{
public CryptoRandomFixture()
{
diff --git a/AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs
new file mode 100644
index 0000000..461f097
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs
@@ -0,0 +1,18 @@
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Services;
+
+namespace AdvancedSystems.Security.Tests.Fixtures;
+
+public sealed class HMACFixture
+{
+ public HMACFixture()
+ {
+ this.HMACService = new HMACService();
+ }
+
+ #region Properties
+
+ public IHMACService HMACService { get; private set; }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs
index 173bd70..3d77817 100644
--- a/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs
+++ b/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs
@@ -7,7 +7,7 @@
namespace AdvancedSystems.Security.Tests.Fixtures;
-public class HashServiceFixture
+public sealed class HashServiceFixture
{
public HashServiceFixture()
{
diff --git a/AdvancedSystems.Security.Tests/Fixtures/KDFServiceFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/KDFServiceFixture.cs
new file mode 100644
index 0000000..6a5f3d9
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Fixtures/KDFServiceFixture.cs
@@ -0,0 +1,18 @@
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Services;
+
+namespace AdvancedSystems.Security.Tests.Fixtures;
+
+public sealed class KDFServiceFixture
+{
+ public KDFServiceFixture()
+ {
+ this.KDFService = new KDFService();
+ }
+
+ #region Properties
+
+ public IKDFService KDFService { get; private set; }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Tests/Helpers/Assets.cs b/AdvancedSystems.Security.Tests/Helpers/Assets.cs
new file mode 100644
index 0000000..42685de
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Helpers/Assets.cs
@@ -0,0 +1,18 @@
+using System;
+using System.IO;
+
+namespace AdvancedSystems.Security.Tests.Helpers;
+
+internal static class Assets
+{
+ internal static string ProjectRoot
+ {
+ get
+ {
+ DirectoryInfo projectRoot = Directory.GetParent(Environment.CurrentDirectory)?.Parent?.Parent?.Parent
+ ?? throw new DirectoryNotFoundException("Failed to locate the project root directory.");
+
+ return projectRoot.FullName;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs b/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs
new file mode 100644
index 0000000..6c7605f
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs
@@ -0,0 +1,9 @@
+namespace AdvancedSystems.Security.Tests.Helpers;
+
+internal static class UserSecrets
+{
+ ///
+ /// Retrieves the certificate password from the secrets store.
+ ///
+ public const string CERTIFICATE_PASSWORD = "CertificatePassword";
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Tests/Interop/LibsodiumTests.cs b/AdvancedSystems.Security.Tests/Interop/LibsodiumTests.cs
new file mode 100644
index 0000000..4314d8c
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Interop/LibsodiumTests.cs
@@ -0,0 +1,69 @@
+using System;
+
+using AdvancedSystems.Security.Cryptography;
+
+using Xunit;
+
+namespace AdvancedSystems.Security.Tests.Interop;
+
+///
+/// Tests that the interop code from libsodium can be invoked without
+/// failure through the class.
+///
+public sealed class LibsodiumTests
+{
+ #region Tests
+
+ ///
+ /// Tests that returns the latest stable
+ /// version of the libsodium NuGet package.
+ ///
+ [Fact]
+ public void TestVersion()
+ {
+ // Arrange
+ var expectedVersion = new Version(1, 0, 20);
+
+ // Act
+ Version actualVersion = Libsodium.Version;
+
+ // Assert
+ Assert.Equal(expectedVersion, actualVersion);
+ }
+
+ ///
+ /// Tests that returns the latest
+ /// major version of the underlying libsodium DLL.
+ ///
+ [Fact]
+ public void TestMajorVersion()
+ {
+ // Arrange
+ int expectedMajorVersion = 26;
+
+ // Act
+ int actualMajorVersion = Libsodium.MajorVersion;
+
+ // Assert
+ Assert.Equal(expectedMajorVersion, actualMajorVersion);
+ }
+
+ ///
+ /// Tests that returns the latest
+ /// minor version of the underlying libsodium DLL.
+ ///
+ [Fact]
+ public void TestMinorVersion()
+ {
+ // Arrange
+ int expectedMinorVersion = 2;
+
+ // Act
+ int actualMinorVersion = Libsodium.MinorVersion;
+
+ // Assert
+ Assert.Equal(expectedMinorVersion, actualMinorVersion);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs
index 3a4d706..e4ad01d 100644
--- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs
+++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs
@@ -1,11 +1,15 @@
-using System.Linq;
+using System;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using AdvancedSystems.Security.Abstractions;
-using AdvancedSystems.Security.Options;
using AdvancedSystems.Security.Tests.Fixtures;
+using AdvancedSystems.Security.Tests.Helpers;
-using Moq;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
using Xunit;
@@ -18,127 +22,162 @@ public sealed class CertificateServiceTests : IClassFixture
{
private readonly CertificateFixture _sut;
- public CertificateServiceTests(CertificateFixture fixture)
+ public CertificateServiceTests(CertificateFixture certificateFixture)
{
- this._sut = fixture;
+ this._sut = certificateFixture;
}
#region Tests
///
- /// Tests that
- /// returns a mocked certificate from the certificate store.
+ /// Tests that
+ /// successfully imports a password-protected PEM certificate.
///
[Fact]
- public void TestGetStoreCertificate()
+ public void TestTryImportPemCertificate_WithPassword()
{
// Arrange
- var certificates = CertificateFixture.CreateCertificateCollection(3);
- string thumbprint = certificates.Select(x => x.Thumbprint).First();
- this._sut.Store.Setup(x => x.Certificates)
- .Returns(certificates);
+ string storeService = this._sut.ConfiguredStoreService;
+ string publicKey = Path.Combine(Assets.ProjectRoot, "development", "AdvancedSystems-PasswordCertificate.pem");
+ string privateKey = Path.Combine(Assets.ProjectRoot, "development", "AdvancedSystems-PrivateKey.pk8");
+
+ var configuration = new ConfigurationBuilder()
+ .AddUserSecrets()
+ .Build();
+
+ string? password = configuration[UserSecrets.CERTIFICATE_PASSWORD];
+ Skip.If(string.IsNullOrEmpty(password), "A dotnet user-secrets is not configured for this test.");
// Act
- var certificate = this._sut.CertificateService.GetStoreCertificate(thumbprint, StoreName.My, StoreLocation.CurrentUser);
+ ICertificateService? certificateService = this._sut.Host?.Services.GetService();
+ bool? isImported = certificateService?.TryImportPemCertificate(storeService, publicKey, privateKey, password, out _);
// Assert
Assert.Multiple(() =>
{
- Assert.NotNull(certificate);
- Assert.Equal(thumbprint, certificate.Thumbprint);
+ Assert.NotNull(certificateService);
+ Assert.True(isImported.HasValue);
+ Assert.True(isImported.Value);
});
-
- this._sut.Store.Verify(service => service.Open(It.Is(flags => flags == OpenFlags.ReadOnly)));
}
///
- /// Tests that
- /// returns if a certificate could not be found in the certificate store.
+ /// Tests that
+ /// successfully imports a password-protected PFX certificate.
///
- [Fact]
- public void TestGetStoreCertificate_NotFound()
+ [SkippableFact]
+ public void TestTryImportPfxCertificate()
{
// Arrange
- string thumbprint = "123456789";
- var storeName = StoreName.My;
- var storeLocation = StoreLocation.CurrentUser;
- this._sut.Store.Setup(x => x.Certificates)
- .Returns(new X509Certificate2Collection());
+ string storeService = this._sut.ConfiguredStoreService;
+ string path = Path.Combine(Assets.ProjectRoot, "development", "AdvancedSystems-CA.pfx");
+
+ var configuration = new ConfigurationBuilder()
+ .AddUserSecrets()
+ .Build();
+
+ string? password = configuration[UserSecrets.CERTIFICATE_PASSWORD];
+ Skip.If(string.IsNullOrEmpty(password), "A dotnet user-secrets is not configured for this test.");
// Act
- var certificate = this._sut.CertificateService.GetStoreCertificate(thumbprint, storeName, storeLocation);
+ ICertificateService? certificateService = this._sut.Host?.Services.GetService();
+ bool? isImported = certificateService?.TryImportPfxCertificate(storeService, path, password, out _);
// Assert
- Assert.Null(certificate);
+ Assert.Multiple(() =>
+ {
+ Assert.NotNull(certificateService);
+ Assert.True(isImported.HasValue);
+ Assert.True(isImported.Value);
+ });
}
///
- /// Tests that
- /// returns a mocked certificate from the certificate store.
+ /// Tests that
+ /// returns a collection of certificates from the configured certificate1 store.
///
[Fact]
- public void GetConfiguredCertificate()
+ public void TestGetCertificate()
{
// 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,
- }
- };
+ string storeService = this._sut.ConfiguredStoreService;
- this._sut.Options.Setup(x => x.Value)
- .Returns(certificateOptions);
+ // Act
+ ICertificateService? certificateService = this._sut.Host?.Services.GetService();
+ int? certificateCount = certificateService?.GetCertificate(storeService).Count();
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.NotNull(certificateCount);
+ Assert.InRange(certificateCount.Value, 1, int.MaxValue);
+ });
+ }
- this._sut.Store.Setup(x => x.Certificates)
- .Returns(certificates);
+ ///
+ /// Tests that
+ /// returns a certificate1 from the certificate1 store.
+ ///
+ [Fact]
+ public void TestGetCertificate_ByThumbprint()
+ {
+ // Arrange
+ string storeService = this._sut.ConfiguredStoreService;
+ string thumbprint = "2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC";
// Act
- var certificate = this._sut.CertificateService.GetConfiguredCertificate();
+ var certificateService = this._sut.Host?.Services.GetService();
+ X509Certificate2? certificate = certificateService?.GetCertificate(storeService, thumbprint, validOnly: false);
// Assert
Assert.Multiple(() =>
{
Assert.NotNull(certificate);
- Assert.Equal(certificateOptions.Thumbprint, certificate.Thumbprint);
+ Assert.Equal(thumbprint, certificate.Thumbprint);
});
-
- this._sut.Store.Verify(service => service.Open(It.Is(flags => flags == OpenFlags.ReadOnly)));
}
///
- /// Tests that
- /// returns if a certificate could not be found in the certificate store.
+ /// Tests that
+ /// adds a self-signed test certificate to the certificate store, and that subsequently
+ /// removes the
+ /// self-signed test certificate from the certificate store.
///
[Fact]
- public void GetConfiguredCertificate_NotFound()
+ public void TestAddRemoveCertificate()
{
// 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());
+ string storeService = this._sut.ConfiguredStoreService;
+ var certificate1 = CreateSelfSignedCertificate("O=AdvancedSystems");
+ string thumbprint = certificate1.Thumbprint;
// Act
- var certificate = this._sut.CertificateService.GetConfiguredCertificate();
+ var certificateService = this._sut.Host?.Services.GetService();
+ bool isAdded = certificateService?.AddCertificate(storeService, certificate1) ?? false;
+ X509Certificate2? certificate2 = certificateService?.GetCertificate(storeService, thumbprint, validOnly: false);
+ bool isRemoved = certificateService?.RemoveCertificate(storeService, thumbprint) ?? false;
// Assert
- Assert.Null(certificate);
+ Assert.Multiple(() =>
+ {
+ Assert.NotNull(certificateService);
+ Assert.True(isAdded);
+ Assert.NotNull(certificate2);
+ Assert.True(isRemoved);
+ });
+ }
+
+ #endregion
+
+ #region Helpers
+
+ private static X509Certificate2 CreateSelfSignedCertificate(string subject)
+ {
+ using var csdsa = ECDsa.Create();
+ var request = new CertificateRequest(subject, csdsa, HashAlgorithmName.SHA256);
+ var validFrom = DateTimeOffset.UtcNow;
+ X509Certificate2 certificate = request.CreateSelfSigned(validFrom, validFrom.AddHours(1));
+ return certificate;
}
#endregion
diff --git a/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs
new file mode 100644
index 0000000..7c773ca
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs
@@ -0,0 +1,48 @@
+using System;
+
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Cryptography;
+using AdvancedSystems.Security.Extensions;
+using AdvancedSystems.Security.Tests.Fixtures;
+
+using Xunit;
+
+namespace AdvancedSystems.Security.Tests.Services;
+
+///
+/// Tests the public methods in .
+///
+public sealed class HMACServiceTests : IClassFixture, IClassFixture
+{
+ private readonly IHMACService _sut;
+ private readonly ICryptoRandomService _cryptoRandomService;
+
+ public HMACServiceTests(HMACFixture fixture, CryptoRandomFixture cryptoRandomFixture)
+ {
+ this._sut = fixture.HMACService;
+ this._cryptoRandomService = cryptoRandomFixture.CryptoRandomService;
+ }
+
+ #region Tests
+
+ ///
+ /// Tests that
+ /// returns a non-empty result.
+ ///
+ [Fact]
+ public void TestCompute()
+ {
+ // Arrange
+ var sha256 = HashFunction.SHA256;
+ Span key = this._cryptoRandomService.GetBytes(32);
+ byte[] data = "Hello, World".GetBytes(Format.String);
+
+ // Act
+ byte[] mac = this._sut.Compute(sha256, key, data);
+
+ // Assert
+ Assert.NotEmpty(mac);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs
index 243a4c5..3984d09 100644
--- a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs
+++ b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs
@@ -2,6 +2,8 @@
using System.Text;
using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Cryptography;
+using AdvancedSystems.Security.Extensions;
using AdvancedSystems.Security.Tests.Fixtures;
using Microsoft.Extensions.Logging;
@@ -30,108 +32,41 @@ public HashServiceTests(HashServiceFixture fixture)
#region Tests
///
- /// Tests that returns the expected hash,
- /// and that the log warning message is called.
+ /// Tests that returns the expected hash,
+ /// and that the log warning message is called on or .
///
- [Fact]
- public void TestMD5Hash()
+ ///
+ ///
+ ///
+ [Theory]
+ [InlineData("The quick brown fox jumps over the lazy dog", "9e107d9d372bb6826bd81d3542a419d6", HashFunction.MD5)]
+ [InlineData("The quick brown fox jumps over the lazy dog", "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", HashFunction.SHA1)]
+ [InlineData("The quick brown fox jumps over the lazy dog", "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", HashFunction.SHA256)]
+ [InlineData("The quick brown fox jumps over the lazy dog", "ca737f1014a48f4c0b6dd43cb177b0afd9e5169367544c494011e3317dbf9a509cb1e5dc1e85a941bbee3d7f2afbc9b1", HashFunction.SHA384)]
+ [InlineData("The quick brown fox jumps over the lazy dog", "07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6", HashFunction.SHA512)]
+ public void TestCompute(string input, string expectedHash, HashFunction hashFunction)
{
// Arrange
- string input = "The quick brown fox jumps over the lazy dog";
byte[] buffer = Encoding.UTF8.GetBytes(input);
+ this._sut.Logger.Invocations.Clear();
// Act
-#pragma warning disable CS0618 // Type or member is obsolete
- string md5 = this._sut.HashService.GetMD5Hash(buffer);
-#pragma warning restore CS0618 // Type or member is obsolete
+ string actualHash = this._sut.HashService.Compute(hashFunction, buffer).ToString(Format.Hex);
// Assert
- Assert.Equal("9e107d9d372bb6826bd81d3542a419d6", md5);
- this._sut.Logger.Verify(x => x.Log(
- LogLevel.Warning,
- It.IsAny(),
- It.Is((v, t) => v.ToString()!.StartsWith("Computing hash with a cryptographically insecure hash algorithm")),
- It.IsAny(),
- It.Is>((v, t) => true))
- );
- }
-
- ///
- /// Tests that returns the expected hash,
- /// and that the log warning message is called.
- ///
- [Fact]
- public void TestSHA1Hash()
- {
- // Arrange
- string input = "The quick brown fox jumps over the lazy dog";
- byte[] buffer = Encoding.UTF8.GetBytes(input);
-
- // Act
-#pragma warning disable CS0618 // Type or member is obsolete
- string sha1 = this._sut.HashService.GetSHA1Hash(buffer);
-#pragma warning restore CS0618 // Type or member is obsolete
-
- // Assert
- Assert.Equal("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", sha1);
- this._sut.Logger.Verify(x => x.Log(
- LogLevel.Warning,
- It.IsAny(),
- It.Is((v, t) => v.ToString()!.StartsWith("Computing hash with a cryptographically insecure hash algorithm")),
- It.IsAny(),
- It.Is>((v, t) => true))
- );
- }
+ Assert.Equal(expectedHash, actualHash);
+
+ if (hashFunction is HashFunction.MD5 or HashFunction.SHA1)
+ {
+ this._sut.Logger.Verify(x => x.Log(
+ LogLevel.Warning,
+ It.IsAny(),
+ It.Is((v, t) => v.ToString()!.StartsWith("Computing hash with a cryptographically insecure hash algorithm")),
+ It.IsAny(),
+ It.Is>((v, t) => true))
+ );
+ }
- ///
- /// Tests that returns the expected hash.
- ///
- [Fact]
- public void TestSHA256Hash()
- {
- // Arrange
- string input = "The quick brown fox jumps over the lazy dog";
- byte[] buffer = Encoding.UTF8.GetBytes(input);
-
- // Act
- string sha256 = this._sut.HashService.GetSHA256Hash(buffer);
-
- // Assert
- Assert.Equal("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", sha256);
- }
-
- ///
- /// Tests that returns the expected hash.
- ///
- [Fact]
- public void TestSHA384Hash()
- {
- // Arrange
- string input = "The quick brown fox jumps over the lazy dog";
- byte[] buffer = Encoding.UTF8.GetBytes(input);
-
- // Act
- string sha384 = this._sut.HashService.GetSHA384Hash(buffer);
-
- // Assert
- Assert.Equal("ca737f1014a48f4c0b6dd43cb177b0afd9e5169367544c494011e3317dbf9a509cb1e5dc1e85a941bbee3d7f2afbc9b1", sha384);
- }
-
- ///
- /// Tests that returns the expected hash.
- ///
- [Fact]
- public void TestSHA512Hash()
- {
- // Arrange
- string input = "The quick brown fox jumps over the lazy dog";
- byte[] buffer = Encoding.UTF8.GetBytes(input);
-
- // Act
- string sha512 = this._sut.HashService.GetSHA512Hash(buffer);
-
- // Assert
- Assert.Equal("07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6", sha512);
}
#endregion
diff --git a/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs b/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs
new file mode 100644
index 0000000..2683601
--- /dev/null
+++ b/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs
@@ -0,0 +1,50 @@
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Cryptography;
+using AdvancedSystems.Security.Extensions;
+using AdvancedSystems.Security.Tests.Fixtures;
+
+using Xunit;
+
+namespace AdvancedSystems.Security.Tests.Services;
+
+///
+/// Tests the public methods in .
+///
+public sealed class KDFServiceTests : IClassFixture
+{
+ private readonly IKDFService _sut;
+
+ public KDFServiceTests(KDFServiceFixture kdfServiceFixture)
+ {
+ this._sut = kdfServiceFixture.KDFService;
+ }
+
+ #region Tests
+
+ ///
+ /// Tests that
+ /// returns a non-empty hash with success state .
+ ///
+ [Fact]
+ public void TestTryComputePBKDF2()
+ {
+ // Arrange
+ var sha256 = HashFunction.SHA256;
+ int iterations = 30_000;
+ int saltSize = 128;
+ byte[] password = "REDACTED".GetBytes(Format.String);
+ byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray();
+
+ // Act
+ bool success = this._sut.TryComputePBKDF2(sha256, password, salt, sha256.GetSize(), iterations, out byte[]? pbkdf2);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.True(success);
+ Assert.NotNull(pbkdf2);
+ });
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security.Tests/appsettings.json b/AdvancedSystems.Security.Tests/appsettings.json
index bd97412..725280e 100644
--- a/AdvancedSystems.Security.Tests/appsettings.json
+++ b/AdvancedSystems.Security.Tests/appsettings.json
@@ -1,9 +1,6 @@
{
- "Certificate": {
- "Thumbprint": "123456789",
- "Store": {
- "Location": "CurrentUser",
- "Name": "My"
- }
+ "CertificateStore": {
+ "Location": "CurrentUser",
+ "Name": "My"
}
}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/AdvancedSystems.Security.csproj b/AdvancedSystems.Security/AdvancedSystems.Security.csproj
index 11b168d..5c765a1 100644
--- a/AdvancedSystems.Security/AdvancedSystems.Security.csproj
+++ b/AdvancedSystems.Security/AdvancedSystems.Security.csproj
@@ -11,13 +11,18 @@
-
+
+
+
+
+
+
diff --git a/AdvancedSystems.Security/Common/Bytes.cs b/AdvancedSystems.Security/Common/Bytes.cs
deleted file mode 100644
index c38e6d1..0000000
--- a/AdvancedSystems.Security/Common/Bytes.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System;
-using System.Numerics;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Security;
-using System.Text;
-
-namespace AdvancedSystems.Security.Common;
-
-///
-/// Implements various utility methods for manipulating byte arrays and spans in cryptographic operations.
-///
-public static class Bytes
-{
- public static string ToString(this byte[] array, Format format)
- {
- return format switch
- {
- Format.Hex => BitConverter.ToString(array).Replace("-", string.Empty).ToLower(),
- Format.Base64 => Convert.ToBase64String(array),
- Format.String => Encoding.UTF8.GetString(array),
- _ => throw new NotSupportedException($"String formatting is not implemted for {format}.")
- };
- }
-
- public static byte[] GetBytes(T value) where T : INumber => value switch
- {
- bool boolValue => BitConverter.GetBytes(boolValue),
- char charValue => BitConverter.GetBytes(charValue),
- double doubleValue => BitConverter.GetBytes(doubleValue),
- Half halfValue => BitConverter.GetBytes(halfValue),
- short shortValue => BitConverter.GetBytes(shortValue),
- int intValue => BitConverter.GetBytes(intValue),
- long longValue => BitConverter.GetBytes(longValue),
- float floatValue => BitConverter.GetBytes(floatValue),
- ushort ushortValue => BitConverter.GetBytes(ushortValue),
- uint uintValue => BitConverter.GetBytes(uintValue),
- ulong ulongValue => BitConverter.GetBytes(ulongValue),
- _ => throw new ArgumentException("Failed to convert T to an array of bytes.", nameof(value))
- };
-
- ///
- /// Gets the bytes of a object.
- ///
- ///
- /// Represents a text that should be kept confidential, such as a password.
- ///
- ///
- /// The character encoding used to convert all characters in the underlying
- /// into a sequence of bytes.
- ///
- ///
- /// Returns the bytes of the current instance.
- ///
- ///
- /// This method exposes the internal state of through marshaling.
- /// It is no longer recommended to use the for new development
- /// on .NET Core projects.
- ///
- ///
- /// Thrown when is null.
- ///
- ///
- [Obsolete]
- public static byte[] GetBytes(this SecureString secureString, Encoding? encoding = null)
- {
- ArgumentNullException.ThrowIfNull(secureString, nameof(secureString));
- encoding ??= Encoding.UTF8;
- IntPtr strPtr = IntPtr.Zero;
-
- try
- {
- strPtr = Marshal.SecureStringToGlobalAllocUnicode(secureString);
- return encoding.GetBytes(Marshal.PtrToStringUni(strPtr)!);
- }
- finally
- {
- if (strPtr != IntPtr.Zero)
- {
- Marshal.ZeroFreeGlobalAllocUnicode(strPtr);
- }
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool Equals(this byte[] lhs, byte[] rhs)
- {
- return new Span(lhs).SequenceEqual(rhs);
- }
-}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs b/AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs
index f300911..c300d0f 100644
--- a/AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs
+++ b/AdvancedSystems.Security/Cryptography/CryptoRandomProvider.cs
@@ -6,6 +6,9 @@
namespace AdvancedSystems.Security.Cryptography;
+///
+/// Represents a class for performing cryptographically secure numerical operations.
+///
public static class CryptoRandomProvider
{
///
@@ -39,7 +42,7 @@ public static void Shuffle(Span values)
///
public static T Choice(Span values)
{
- int index = CryptoRandomProvider.GetInt32(0, values.Length);
+ int index = CryptoRandomProvider.GetInt32(0, values.Length - 1);
return values[index];
}
}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Cryptography/DistinguishedName.cs b/AdvancedSystems.Security/Cryptography/DistinguishedName.cs
index 5d07ce1..84351d9 100644
--- a/AdvancedSystems.Security/Cryptography/DistinguishedName.cs
+++ b/AdvancedSystems.Security/Cryptography/DistinguishedName.cs
@@ -52,7 +52,7 @@ public sealed record DistinguishedName
public string? OrganizationalUnit { get; init; }
///
- /// Gets or sets the ..
+ /// Gets or sets the .
///
///
///
diff --git a/AdvancedSystems.Security/Common/Format.cs b/AdvancedSystems.Security/Cryptography/Format.cs
similarity index 92%
rename from AdvancedSystems.Security/Common/Format.cs
rename to AdvancedSystems.Security/Cryptography/Format.cs
index 0d4aaa0..ad34d1c 100644
--- a/AdvancedSystems.Security/Common/Format.cs
+++ b/AdvancedSystems.Security/Cryptography/Format.cs
@@ -1,4 +1,4 @@
-namespace AdvancedSystems.Security.Common;
+namespace AdvancedSystems.Security.Cryptography;
///
/// Describes string formatting options for byte arrays.
diff --git a/AdvancedSystems.Security/Cryptography/HMACProvider.cs b/AdvancedSystems.Security/Cryptography/HMACProvider.cs
new file mode 100644
index 0000000..9dc9a41
--- /dev/null
+++ b/AdvancedSystems.Security/Cryptography/HMACProvider.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Security.Cryptography;
+
+using AdvancedSystems.Security.Abstractions;
+
+namespace AdvancedSystems.Security.Cryptography;
+
+///
+/// Represents a class designed for computing Hash-Based Message Authentication Codes (HMAC).
+///
+public static class HMACProvider
+{
+ ///
+ public static byte[] Compute(HashFunction hashFunction, ReadOnlySpan key, ReadOnlySpan buffer)
+ {
+ return hashFunction switch
+ {
+ HashFunction.MD5 => HMACMD5.HashData(key, buffer),
+ HashFunction.SHA1 => HMACSHA1.HashData(key, buffer),
+ HashFunction.SHA256 => HMACSHA256.HashData(key, buffer),
+ HashFunction.SHA384 => HMACSHA384.HashData(key, buffer),
+ HashFunction.SHA512 => HMACSHA512.HashData(key, buffer),
+ HashFunction.SHA3_256 => HMACSHA3_256.HashData(key, buffer),
+ HashFunction.SHA3_384 => HMACSHA3_384.HashData(key, buffer),
+ HashFunction.SHA3_512 => HMACSHA3_512.HashData(key, buffer),
+ _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."),
+ };
+ }
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Cryptography/Hash.cs b/AdvancedSystems.Security/Cryptography/Hash.cs
deleted file mode 100644
index 3af5f0e..0000000
--- a/AdvancedSystems.Security/Cryptography/Hash.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using System.Runtime.CompilerServices;
-using System.Security.Cryptography;
-
-namespace AdvancedSystems.Security.Cryptography;
-
-///
-/// Implements cryptographic hash algorithms.
-///
-public static class Hash
-{
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static HashAlgorithm Create(HashAlgorithmName hashAlgorithmName)
- {
- // Starting with net8, cryptographic factory methods accepting an algorithm
- // name are obsolet and to be replaced by the parameterless Create factory
- // method on the algorithm type, because their derived cryptographic types
- // such as SHA1Managed were obsoleted with net6. That is why this function
- // is intentionally not using the HashAlgorithmName.Create(name) factory.
- return hashAlgorithmName.Name switch
- {
- "MD5" => MD5.Create(),
- "SHA1" => SHA1.Create(),
- "SHA256" => SHA256.Create(),
- "SHA384" => SHA384.Create(),
- "SHA512" => SHA512.Create(),
- "SHA3-256" => SHA3_256.Create(),
- "SHA3-384" => SHA3_384.Create(),
- "SHA3-512" => SHA3_512.Create(),
- _ => throw new NotImplementedException()
- };
- }
-
- ///
- /// Computes the hash value for the specified byte array.
- ///
- ///
- /// The input to compute the hash code for.
- ///
- ///
- /// The hash algorithm implementation to use.
- ///
- ///
- /// The computed hash code.
- ///
- public static byte[] Compute(byte[] buffer, HashAlgorithmName hashAlgorithmName)
- {
- using var hashAlgorithm = Hash.Create(hashAlgorithmName);
- return hashAlgorithm.ComputeHash(buffer);
- }
-}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Cryptography/HashProvider.cs b/AdvancedSystems.Security/Cryptography/HashProvider.cs
new file mode 100644
index 0000000..a96fa9b
--- /dev/null
+++ b/AdvancedSystems.Security/Cryptography/HashProvider.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Security.Cryptography;
+
+using AdvancedSystems.Security.Abstractions;
+
+namespace AdvancedSystems.Security.Cryptography;
+
+///
+/// Represents a class designed for computing hash algorithms.
+///
+public static class HashProvider
+{
+ ///
+ public static byte[] Compute(HashFunction hashFunction, byte[] buffer)
+ {
+ return hashFunction switch
+ {
+ HashFunction.MD5 => MD5.HashData(buffer),
+ HashFunction.SHA1 => SHA1.HashData(buffer),
+ HashFunction.SHA256 => SHA256.HashData(buffer),
+ HashFunction.SHA384 => SHA384.HashData(buffer),
+ HashFunction.SHA512 => SHA512.HashData(buffer),
+ HashFunction.SHA3_256 => SHA3_256.HashData(buffer),
+ HashFunction.SHA3_384 => SHA3_384.HashData(buffer),
+ HashFunction.SHA3_512 => SHA3_512.HashData(buffer),
+ _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."),
+ };
+ }
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Cryptography/KDFProvider.cs b/AdvancedSystems.Security/Cryptography/KDFProvider.cs
new file mode 100644
index 0000000..41dbc5f
--- /dev/null
+++ b/AdvancedSystems.Security/Cryptography/KDFProvider.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Security.Cryptography;
+
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Extensions;
+
+namespace AdvancedSystems.Security.Cryptography;
+
+///
+/// Represents a class designed for employing key derivation functions.
+///
+public static class KDFProvider
+{
+ ///
+ public static bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2)
+ {
+ try
+ {
+ pbkdf2 = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, hashFunction.ToHashAlgorithmName(), hashSize);
+ return true;
+ }
+ catch (Exception)
+ {
+ pbkdf2 = null;
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Cryptography/Libsodium.Version.cs b/AdvancedSystems.Security/Cryptography/Libsodium.Version.cs
new file mode 100644
index 0000000..c9ee96a
--- /dev/null
+++ b/AdvancedSystems.Security/Cryptography/Libsodium.Version.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Runtime.InteropServices;
+
+using static AdvancedSystems.Security.Interop.Libsodium;
+
+namespace AdvancedSystems.Security.Cryptography;
+
+///
+/// A .NET wrapper class that provides bindings to the native libsodium library.
+///
+///
+/// See also: .
+///
+public static partial class Libsodium
+{
+ #region Properties
+
+ ///
+ /// Gets the version number of the underlying libsodium NuGet package.
+ ///
+ ///
+ /// Raised if the native code execution failed to retrieve the version number.
+ ///
+ ///
+ ///
+ ///
+ public static Version Version
+ {
+ get
+ {
+ string? version = Marshal.PtrToStringAnsi(SodiumVersionString());
+ ArgumentNullException.ThrowIfNull(version, nameof(version));
+
+ return new Version(version);
+ }
+ }
+
+ ///
+ /// Gets the major version number of the underlying native libsodium DLL.
+ ///
+ public static int MajorVersion
+ {
+ get
+ {
+ return SodiumLibraryVersionMajor();
+ }
+ }
+
+ ///
+ /// Gets the minor version number of the underlying native libsodium DLL.
+ ///
+ public static int MinorVersion
+ {
+ get
+ {
+ return SodiumLibraryVersionMinor();
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs
index 4db4394..656910e 100644
--- a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs
+++ b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs
@@ -7,6 +7,9 @@
namespace AdvancedSystems.Security.Cryptography;
+///
+/// Represents a class for performing RSA-based asymmetric operations.
+///
public sealed class RSACryptoProvider
{
private static readonly HashAlgorithmName DEFAULT_HASH_ALGORITHM_NAME = HashAlgorithmName.SHA256;
@@ -48,27 +51,6 @@ public RSACryptoProvider(X509Certificate2 certificate)
#region Public Methods
- public bool IsValidMessage(string message, RSAEncryptionPadding? padding, Encoding? encoding = null)
- {
- using RSA? publicKey = this.Certificate.GetRSAPublicKey();
- ArgumentNullException.ThrowIfNull(publicKey, nameof(publicKey));
-
- encoding ??= this.Encoding;
- int messageSize = encoding.GetByteCount(message);
- decimal keySize = Math.Floor(publicKey.KeySize / 8M);
-
- if (padding is null)
- {
- decimal limit = keySize - 11;
- return messageSize <= limit;
- }
-
- using var hashAlgorithm = Hash.Create(padding.OaepHashAlgorithm);
- decimal hashSize = Math.Ceiling(hashAlgorithm.HashSize / 8M);
- decimal limitPadded = keySize - (2 * hashSize) - 2;
- return messageSize <= limitPadded;
- }
-
public string Encrypt(string message, Encoding? encoding = null)
{
encoding ??= this.Encoding;
diff --git a/AdvancedSystems.Security/DependencyInjection/Sections.cs b/AdvancedSystems.Security/DependencyInjection/Sections.cs
index 21a9db9..f08e1d5 100644
--- a/AdvancedSystems.Security/DependencyInjection/Sections.cs
+++ b/AdvancedSystems.Security/DependencyInjection/Sections.cs
@@ -7,18 +7,13 @@ namespace AdvancedSystems.Security.DependencyInjection;
///
public static partial class Sections
{
- ///
- /// Key used to bind the configuration section.
- ///
- public const string CERTIFICATE = "Certificate";
-
///
/// Key used to bind the configuration section.
///
public const string RSA = "RSA";
///
- /// Key used to bind the configuration section.
+ /// Key used to bind the configuration section.
///
- public const string STORE = "Store";
+ public const string CERTIFICATE_STORE = "CertificateStore";
}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs
index 5dac396..714f54c 100644
--- a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using System.IO.Abstractions;
using AdvancedSystems.Core.DependencyInjection;
using AdvancedSystems.Security.Abstractions;
@@ -14,10 +15,10 @@ namespace AdvancedSystems.Security.DependencyInjection;
public static partial class ServiceCollectionExtensions
{
- #region CryptoRandom
+ #region CertificateService
///
- /// Adds the default implementation of to .
+ /// Adds the default implementation of to .
///
///
/// The service collection containing the service.
@@ -25,28 +26,11 @@ public static partial class ServiceCollectionExtensions
///
/// The value of .
///
- public static IServiceCollection AddCryptoRandomService(this IServiceCollection services)
+ public static IServiceCollection AddCertificateService(this IServiceCollection services)
{
- services.TryAdd(ServiceDescriptor.Scoped());
- return services;
- }
-
- #endregion
-
- #region HashService
+ services.TryAdd(ServiceDescriptor.Singleton());
+ services.TryAdd(ServiceDescriptor.Scoped());
- ///
- /// Adds the default implementation of to .
- ///
- ///
- /// The service collection containing the service.
- ///
- ///
- /// The value of .
- ///
- public static IServiceCollection AddHashService(this IServiceCollection services)
- {
- services.TryAdd(ServiceDescriptor.Scoped());
return services;
}
@@ -54,19 +38,15 @@ public static IServiceCollection AddHashService(this IServiceCollection services
#region CertificateStore
- private static void AddCertificateStore(this IServiceCollection services) where TOptions : class
+ private static IServiceCollection AddCertificateStore(this IServiceCollection services, string key)
{
- services.TryAdd(ServiceDescriptor.Singleton(serviceProvider =>
+ services.TryAdd(ServiceDescriptor.KeyedSingleton(key, (serviceProvider, _) =>
{
- var options = serviceProvider.GetRequiredService>().Value switch
- {
- CertificateOptions certificateOptions => new { certificateOptions.Store.Name, certificateOptions.Store.Location },
- CertificateStoreOptions storeOptions => new { storeOptions.Name, storeOptions.Location },
- _ => throw new NotImplementedException()
- };
-
+ var options = serviceProvider.GetRequiredService>().Value;
return new CertificateStore(options.Name, options.Location);
}));
+
+ return services;
}
///
@@ -75,18 +55,21 @@ private static void AddCertificateStore(this IServiceCollection servic
///
/// The service collection containing the service.
///
+ ///
+ /// Name of the keyed service.
+ ///
///
/// An action used to configure .
///
///
/// The value of .
///
- public static IServiceCollection AddCertificateStore(this IServiceCollection services, Action setupAction)
+ public static IServiceCollection AddCertificateStore(this IServiceCollection services, string key, Action setupAction)
{
services.AddOptions()
.Configure(setupAction);
- services.AddCertificateStore();
+ services.AddCertificateStore(key);
return services;
}
@@ -96,70 +79,95 @@ public static IServiceCollection AddCertificateStore(this IServiceCollection ser
///
/// The service collection containing the service.
///
+ ///
+ /// Name of the keyed service.
+ ///
///
/// A configuration section targeting .
///
///
/// The value of .
///
- public static IServiceCollection AddCertificateStore(this IServiceCollection services, IConfigurationSection configurationSection)
+ public static IServiceCollection AddCertificateStore(this IServiceCollection services, string key, IConfigurationSection configurationSection)
{
services.TryAddOptions(configurationSection);
- services.AddCertificateStore();
+ services.AddCertificateStore(key);
return services;
}
#endregion
- #region CertificateService
+ #region CryptoRandomService
- private static void AddCertificateService(this IServiceCollection services)
+ ///
+ /// Adds the default implementation of to .
+ ///
+ ///
+ /// The service collection containing the service.
+ ///
+ ///
+ /// The value of .
+ ///
+ public static IServiceCollection AddCryptoRandomService(this IServiceCollection services)
{
- services.TryAdd(ServiceDescriptor.Scoped());
+ services.TryAdd(ServiceDescriptor.Scoped());
+ return services;
}
+ #endregion
+
+ #region HashService
+
///
- /// Adds the default implementation of to .
+ /// Adds the default implementation of to .
///
///
/// The service collection containing the service.
///
- ///
- /// An action used to configure .
- ///
///
/// The value of .
///
- public static IServiceCollection AddCertificateService(this IServiceCollection services, Action setupAction)
+ public static IServiceCollection AddHashService(this IServiceCollection services)
{
- services.AddOptions()
- .Configure(setupAction);
+ services.TryAdd(ServiceDescriptor.Scoped());
+ return services;
+ }
+
+ #endregion
- services.AddCertificateStore();
- services.AddCertificateService();
+ #region HMACService
+ ///
+ /// Adds the default implementation of to .
+ ///
+ ///
+ /// The service collection containing the service.
+ ///
+ ///
+ /// The value of .
+ ///
+ public static IServiceCollection AddHMACService(this IServiceCollection services)
+ {
+ services.TryAdd(ServiceDescriptor.Scoped());
return services;
}
+ #endregion
+
+ #region KDFService
+
///
- /// Adds the default implementation of to .
+ /// Adds the default implementation of to .
///
///
/// The service collection containing the service.
///
- ///
- /// A configuration section targeting . NOTE: This configuration requires a nested
- /// section within the previous section.
- ///
///
/// The value of .
///
- public static IServiceCollection AddCertificateService(this IServiceCollection services, IConfigurationSection configurationSection)
+ public static IServiceCollection AddKDFService(this IServiceCollection services)
{
- services.TryAddOptions(configurationSection);
- services.AddCertificateStore(configurationSection.GetRequiredSection(Sections.STORE));
- services.AddCertificateService();
-
+ services.TryAdd(ServiceDescriptor.Scoped());
return services;
}
@@ -186,6 +194,8 @@ private static IServiceCollection AddRSACryptoService(this IServiceCollection se
///
public static IServiceCollection AddRSACryptoService(this IServiceCollection services, Action setupAction)
{
+ services.AddRSACryptoService();
+
throw new NotImplementedException();
}
@@ -203,6 +213,8 @@ public static IServiceCollection AddRSACryptoService(this IServiceCollection ser
///
public static IServiceCollection AddRSACryptoService(this IServiceCollection services, IConfigurationSection configurationSection)
{
+ services.AddRSACryptoService();
+
throw new NotImplementedException();
}
diff --git a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs
index 27086b2..c0dc448 100644
--- a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs
+++ b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs
@@ -1,14 +1,10 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography.X509Certificates;
-using AdvancedSystems.Security.Abstractions;
-using AdvancedSystems.Security.Abstractions.Exceptions;
using AdvancedSystems.Security.Cryptography;
-using static System.Net.WebRequestMethods;
-
namespace AdvancedSystems.Security.Extensions;
///
@@ -17,37 +13,6 @@ namespace AdvancedSystems.Security.Extensions;
///
public static partial class CertificateExtensions
{
- ///
- /// Retrieves an X.509 certificate from the specified store using the provided thumbprint.
- ///
- ///
- /// The type of the certificate store, which must implement the interface.
- ///
- ///
- /// The certificate store from which to retrieve the certificate.
- ///
- ///
- /// The thumbprint of the certificate to locate.
- ///
- ///
- /// The object if the certificate is found.
- ///
- ///
- /// Thrown when no certificate with the specified thumbprint is found in the store.
- ///
- public static X509Certificate2 GetCertificate(this T store, string thumbprint) where T : ICertificateStore
- {
- store.Open(OpenFlags.ReadOnly);
-
- var certificate = store.Certificates
- .Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
- .OfType()
- .FirstOrDefault();
-
- return certificate
- ?? throw new CertificateNotFoundException("No valid certificate matching the search criteria could be found in the store.");
- }
-
///
/// Attempts to parse the specified distinguished name (DN) string into a object.
///
@@ -138,7 +103,7 @@ public static X509Certificate2 GetCertificate(this T store, string thumbprint
///
///
///
- public static bool TryParseDistinguishedName(string distinguishedName, out DistinguishedName? result)
+ public static bool TryParseDistinguishedName(string distinguishedName, [NotNullWhen(true)] out DistinguishedName? result)
{
var rdns = new Dictionary(StringComparer.OrdinalIgnoreCase);
var dn = new X500DistinguishedName(distinguishedName);
diff --git a/AdvancedSystems.Security/Extensions/CoreExtensions.cs b/AdvancedSystems.Security/Extensions/CoreExtensions.cs
new file mode 100644
index 0000000..86327d8
--- /dev/null
+++ b/AdvancedSystems.Security/Extensions/CoreExtensions.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Text;
+
+using AdvancedSystems.Security.Cryptography;
+
+namespace AdvancedSystems.Security.Extensions;
+
+///
+/// Implements extension methods built-in types. See also:
+/// .
+///
+public static class CoreExtensions
+{
+ public static string ToString(this byte[] array, Format format)
+ {
+ return format switch
+ {
+ Format.Hex => Convert.ToHexString(array).ToLower(),
+ Format.Base64 => Convert.ToBase64String(array),
+ Format.String => Encoding.UTF8.GetString(array),
+ _ => throw new NotSupportedException($"Case {format} is not implemented.")
+ };
+ }
+
+ public static byte[] GetBytes(this string @string, Format format)
+ {
+ return format switch
+ {
+ Format.Hex => Convert.FromHexString(@string),
+ Format.Base64 => Convert.FromBase64String(@string),
+ Format.String => Encoding.UTF8.GetBytes(@string),
+ _ => throw new NotSupportedException($"Case {format} is not implemented.")
+ };
+ }
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Extensions/HashFunctionExtensions.cs b/AdvancedSystems.Security/Extensions/HashFunctionExtensions.cs
new file mode 100644
index 0000000..f46eba4
--- /dev/null
+++ b/AdvancedSystems.Security/Extensions/HashFunctionExtensions.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+
+using AdvancedSystems.Security.Abstractions;
+
+namespace AdvancedSystems.Security.Extensions;
+
+public static class HashFunctionExtensions
+{
+ ///
+ /// Converts the specified to its corresponding instance of
+ /// .
+ ///
+ ///
+ /// The specified hash algorithm.
+ ///
+ ///
+ /// An instance of .
+ ///
+ ///
+ /// Raised if the value of cannot be backed by a built-in implementation.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static HashAlgorithmName ToHashAlgorithmName(this HashFunction hashFunction)
+ {
+ return hashFunction switch
+ {
+ HashFunction.MD5 => HashAlgorithmName.MD5,
+ HashFunction.SHA1 => HashAlgorithmName.SHA1,
+ HashFunction.SHA256 => HashAlgorithmName.SHA256,
+ HashFunction.SHA384 => HashAlgorithmName.SHA384,
+ HashFunction.SHA512 => HashAlgorithmName.SHA512,
+ HashFunction.SHA3_256 => HashAlgorithmName.SHA3_256,
+ HashFunction.SHA3_384 => HashAlgorithmName.SHA3_384,
+ HashFunction.SHA3_512 => HashAlgorithmName.SHA3_512,
+ _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."),
+ };
+ }
+
+ ///
+ /// Gets the size, in bits, of the computed hash code.
+ ///
+ ///
+ /// The specified hash algorithm.
+ ///
+ ///
+ /// Raised if the value of cannot be backed by a built-in implementation.
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int GetSize(this HashFunction hashFunction)
+ {
+ return hashFunction switch
+ {
+ HashFunction.MD5 => 128,
+ HashFunction.SHA1 => 160,
+ HashFunction.SHA256 or HashFunction.SHA3_256 => 256,
+ HashFunction.SHA384 or HashFunction.SHA3_384 => 384,
+ HashFunction.SHA512 or HashFunction.SHA3_512 => 512,
+ _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented."),
+ };
+ }
+
+ ///
+ /// Gets the name of the hash function.
+ ///
+ ///
+ /// The specified hash algorithm.
+ ///
+ ///
+ /// The name of the hash function.
+ ///
+ ///
+ /// Raised if the value of cannot be backed by a built-in implementation.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string GetName(this HashFunction hashFunction)
+ {
+ return hashFunction switch
+ {
+ HashFunction.MD5 => "MD5",
+ HashFunction.SHA1 => "SHA1",
+ HashFunction.SHA256 => "SHA256",
+ HashFunction.SHA384 => "SHA384",
+ HashFunction.SHA512 => "SHA512",
+ HashFunction.SHA3_256 => "SHA3-256",
+ HashFunction.SHA3_384 => "SHA3-384",
+ HashFunction.SHA3_512 => "SHA3-512",
+ _ => throw new NotImplementedException($"The hash function {hashFunction} is not implemented.")
+ };
+ }
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Extensions/SecureStringExtensions.cs b/AdvancedSystems.Security/Extensions/SecureStringExtensions.cs
new file mode 100644
index 0000000..59839e4
--- /dev/null
+++ b/AdvancedSystems.Security/Extensions/SecureStringExtensions.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Security;
+using System.Text;
+
+namespace AdvancedSystems.Security.Extensions;
+
+public static class SecureStringExtensions
+{
+ ///
+ /// Gets the bytes of a object.
+ ///
+ ///
+ /// Represents a text that should be kept confidential, such as a password.
+ ///
+ ///
+ /// The character encoding used to convert all characters in the underlying
+ /// into a sequence of bytes.
+ ///
+ ///
+ /// Returns the bytes of the current instance.
+ ///
+ ///
+ ///
+ /// This method exposes the internal state of through marshaling.
+ /// It is no longer recommended to use the for new development
+ /// on .NET CryptoCore projects.
+ ///
+ ///
+ /// See also: .
+ ///
+ ///
+ ///
+ /// Thrown when is .
+ ///
+ [Obsolete]
+ public static byte[] GetBytes(this SecureString secureString, Encoding? encoding = null)
+ {
+ ArgumentNullException.ThrowIfNull(secureString, nameof(secureString));
+ encoding ??= Encoding.UTF8;
+ IntPtr strPtr = IntPtr.Zero;
+
+ try
+ {
+ strPtr = Marshal.SecureStringToGlobalAllocUnicode(secureString);
+ return encoding.GetBytes(Marshal.PtrToStringUni(strPtr)!);
+ }
+ finally
+ {
+ if (strPtr != IntPtr.Zero)
+ {
+ Marshal.ZeroFreeGlobalAllocUnicode(strPtr);
+ }
+ }
+ }
+
+ ///
+ /// Returns an managed derived from the contents of a managed object.
+ ///
+ ///
+ /// Represents a text that should be kept confidential, such as a password.
+ ///
+ ///
+ /// A managed string that holds the contents of the object.
+ ///
+ ///
+ ///
+ ///
+ [Obsolete]
+ public static string ToString(this SecureString secureString)
+ {
+ ArgumentNullException.ThrowIfNull(secureString, nameof(secureString));
+ IntPtr strPtr = IntPtr.Zero;
+
+ try
+ {
+ strPtr = Marshal.SecureStringToBSTR(secureString);
+ return Marshal.PtrToStringBSTR(strPtr);
+ }
+ finally
+ {
+ if (strPtr != IntPtr.Zero)
+ {
+ Marshal.ZeroFreeBSTR(strPtr);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Extensions/StringExtensions.cs b/AdvancedSystems.Security/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..1aba33b
--- /dev/null
+++ b/AdvancedSystems.Security/Extensions/StringExtensions.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+using AdvancedSystems.Security.Cryptography;
+
+namespace AdvancedSystems.Security.Extensions;
+
+public static class StringExtensions
+{
+
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Internals/Hazmat.cs b/AdvancedSystems.Security/Internals/Hazmat.cs
index 41b315a..ea37cdb 100644
--- a/AdvancedSystems.Security/Internals/Hazmat.cs
+++ b/AdvancedSystems.Security/Internals/Hazmat.cs
@@ -3,11 +3,11 @@
namespace AdvancedSystems.Security.Internals;
///
-/// Defines low-level cryptographic primitives that can be dangerous if used incorrectly.
-/// Because of the high risk accociated with working at this level, this is referred to
-/// as the "hazardous materials" or "hazmat" layer.
+/// Functions defined in this class can have dangerous security implications if
+/// used incorrectly. Because of the high risk accociated with working at this level,
+/// this is referred to as the "hazardous materials" (short: "hazmat") layer.
///
-internal static class Hazmat
+public static class Hazmat
{
///
/// Gets an array of type and size
@@ -25,7 +25,7 @@ internal static class Hazmat
/// Skipping zero-initialization using this API only has a material performance benefit for
/// large arrays, such as buffers of several kilobytes or more.
///
- internal static T[] GetUninitializedArray(int size) where T : unmanaged
+ public static T[] GetUninitializedArray(int size) where T : unmanaged
{
// If pinned is set to true, T must not be a reference type or a type that contains object references.
return GC.AllocateUninitializedArray(size, pinned: true);
diff --git a/AdvancedSystems.Security/Interop/Libsodium.Version.cs b/AdvancedSystems.Security/Interop/Libsodium.Version.cs
new file mode 100644
index 0000000..3d368cb
--- /dev/null
+++ b/AdvancedSystems.Security/Interop/Libsodium.Version.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace AdvancedSystems.Security.Interop;
+
+internal static partial class Libsodium
+{
+ [LibraryImport(NativeLibrary.LIBSODIUM, EntryPoint = "sodium_library_version_major")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ internal static partial int SodiumLibraryVersionMajor();
+
+ [LibraryImport(NativeLibrary.LIBSODIUM, EntryPoint = "sodium_library_version_minor")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ internal static partial int SodiumLibraryVersionMinor();
+
+ [LibraryImport(NativeLibrary.LIBSODIUM, EntryPoint = "sodium_version_string")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ internal static partial IntPtr SodiumVersionString();
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Interop/NativeLibrary.cs b/AdvancedSystems.Security/Interop/NativeLibrary.cs
new file mode 100644
index 0000000..fa39b6e
--- /dev/null
+++ b/AdvancedSystems.Security/Interop/NativeLibrary.cs
@@ -0,0 +1,6 @@
+namespace AdvancedSystems.Security.Interop;
+
+internal static partial class NativeLibrary
+{
+ internal const string LIBSODIUM = "libsodium";
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Options/CertificateOptions.cs b/AdvancedSystems.Security/Options/CertificateOptions.cs
deleted file mode 100644
index 505c02b..0000000
--- a/AdvancedSystems.Security/Options/CertificateOptions.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-
-namespace AdvancedSystems.Security.Options;
-
-public sealed record CertificateOptions
-{
- [Key]
- [Required(AllowEmptyStrings = false)]
- public required string Thumbprint { get; set; }
-
- [Required]
- public required CertificateStoreOptions Store { get; set; }
-}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Options/RSACryptoOptions.cs b/AdvancedSystems.Security/Options/RSACryptoOptions.cs
index 3e24f72..6cbea4a 100644
--- a/AdvancedSystems.Security/Options/RSACryptoOptions.cs
+++ b/AdvancedSystems.Security/Options/RSACryptoOptions.cs
@@ -1,15 +1,23 @@
-using System.Security.Cryptography;
+using System.ComponentModel.DataAnnotations;
+using System.Security.Cryptography;
using System.Text;
namespace AdvancedSystems.Security.Options;
public sealed record RSACryptoOptions
{
+ [Required]
public required HashAlgorithmName HashAlgorithmName { get; set; }
+ [Required]
public required RSAEncryptionPadding EncryptionPadding { get; set; }
+ [Required]
public required RSASignaturePadding SignaturePadding { get; set; }
+ [Required]
public required Encoding Encoding { get; set; }
+
+ [Required]
+ public required string Thumbprint { get; set; }
}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Services/CertificateService.cs b/AdvancedSystems.Security/Services/CertificateService.cs
index 9cf09bb..e570f05 100644
--- a/AdvancedSystems.Security/Services/CertificateService.cs
+++ b/AdvancedSystems.Security/Services/CertificateService.cs
@@ -1,52 +1,241 @@
-using System.Security.Cryptography.X509Certificates;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO.Abstractions;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
using AdvancedSystems.Security.Abstractions;
-using AdvancedSystems.Security.Abstractions.Exceptions;
-using AdvancedSystems.Security.Extensions;
-using AdvancedSystems.Security.Options;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-
-using static AdvancedSystems.Core.Common.ExceptionFilter;
namespace AdvancedSystems.Security.Services;
-///
+///
+/// Defines a service for managing and retrieving X.509 certificates.
+///
+///
+///
+///
public sealed class CertificateService : ICertificateService
{
private readonly ILogger _logger;
- private readonly IOptions _certificateOptions;
- private readonly ICertificateStore _certificateStore;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IFileSystem _fileSystem;
- public CertificateService(ILogger logger, IOptions certificateOptions, ICertificateStore certificateStore)
+ public CertificateService(ILogger logger, IServiceProvider serviceProvider, IFileSystem fileSystem)
{
this._logger = logger;
- this._certificateOptions = certificateOptions;
- this._certificateStore = certificateStore;
+ this._serviceProvider = serviceProvider;
+ this._fileSystem = fileSystem;
}
#region Methods
///
- public X509Certificate2? GetStoreCertificate(string thumbprint, StoreName storeName, StoreLocation storeLocation)
+ public bool AddCertificate(string storeService, X509Certificate2 certificate)
{
+ var store = this._serviceProvider.GetRequiredKeyedService(storeService);
+
try
{
- using var _ = this._logger.BeginScope("Searching for {thumbprint} in {storeName} at {storeLocation}", thumbprint, storeName, storeLocation);
- return this._certificateStore.GetCertificate(thumbprint);
+ store.Open(OpenFlags.ReadWrite);
+ store.Add(certificate);
+ return true;
+ }
+ catch (Exception)
+ {
+ this._logger.LogError(
+ "Failed to remove certificate (\"{Thumbprint}\") from store (L='{Location}',N='{Name}').",
+ certificate.Thumbprint,
+ store.Location,
+ store.Name
+ );
+
+ return false;
}
- catch (CertificateNotFoundException exception) when (True(() => this._logger.LogError(exception, "{Service} failed to retrieve certificate.", nameof(CertificateService))))
+ finally
{
- return null;
+ store.Close();
}
}
///
- public X509Certificate2? GetConfiguredCertificate()
+ public bool TryImportPemCertificate(string storeService, string certificatePath, string? privateKeyPath, [NotNullWhen(true)] out X509Certificate2? certificate)
{
- var options = this._certificateOptions.Value;
- return this.GetStoreCertificate(options.Thumbprint, options.Store.Name, options.Store.Location);
+ return this.TryImportPemCertificate(storeService, certificatePath, privateKeyPath, string.Empty, out certificate);
+ }
+
+ ///
+ public bool TryImportPemCertificate(string storeService, string certificatePath, string? privateKeyPath, string password, [NotNullWhen(true)] out X509Certificate2? certificate)
+ {
+ try
+ {
+ bool withPassword = !string.IsNullOrEmpty(password);
+
+ using var pemCertificate = withPassword
+ ? new X509Certificate2(certificatePath, password, KeyStorageFlags)
+ : new X509Certificate2(certificatePath);
+
+ if (!string.IsNullOrEmpty(privateKeyPath))
+ {
+ string pemContent = this._fileSystem.File.ReadAllText(privateKeyPath);
+ using var privateKey = RSA.Create();
+
+ if (withPassword)
+ {
+ privateKey.ImportFromEncryptedPem(pemContent, password);
+ }
+ else
+ {
+ privateKey.ImportFromPem(pemContent);
+ }
+
+ certificate = pemCertificate.CopyWithPrivateKey(privateKey);
+ }
+ else
+ {
+ certificate = pemCertificate;
+ }
+
+ bool isImported = this.AddCertificate(storeService, certificate);
+ return isImported;
+ }
+ catch (Exception exception)
+ {
+ this._logger.LogError(
+ "Failed to initialize public key or private key from path: {Reason}.",
+ exception.Message
+ );
+
+ certificate = null;
+ return false;
+ }
+ }
+
+ ///
+ public bool TryImportPfxCertificate(string storeService, string certificatePath, [NotNullWhen(true)] out X509Certificate2? certificate)
+ {
+ return this.TryImportPfxCertificate(storeService, certificatePath, string.Empty, out certificate);
+ }
+
+ ///
+ public bool TryImportPfxCertificate(string storeService, string certificatePath, string password, [NotNullWhen(true)] out X509Certificate2? certificate)
+ {
+ try
+ {
+ bool withPassword = !string.IsNullOrEmpty(password);
+
+ certificate = withPassword
+ ? new X509Certificate2(certificatePath, password, KeyStorageFlags)
+ : new X509Certificate2(certificatePath);
+
+ bool isImported = this.AddCertificate(storeService, certificate);
+ return isImported;
+ }
+ catch (CryptographicException exception)
+ {
+ this._logger.LogError(
+ "Failed to initialize certificate from path: {Reason}.",
+ exception.Message
+ );
+
+ certificate = null;
+ return false;
+ }
+ }
+
+ ///
+ public IEnumerable GetCertificate(string storeService)
+ {
+ var store = this._serviceProvider.GetRequiredKeyedService(storeService);
+
+ try
+ {
+ store.Open(OpenFlags.ReadOnly);
+
+ return store.Certificates.OfType();
+ }
+ catch (ArgumentNullException)
+ {
+ return Enumerable.Empty();
+ }
+ finally
+ {
+ store.Close();
+ }
+ }
+
+ ///
+ public X509Certificate2? GetCertificate(string storeService, string thumbprint, bool validOnly = true)
+ {
+ var store = this._serviceProvider.GetRequiredKeyedService(storeService);
+
+ try
+ {
+ store.Open(OpenFlags.ReadOnly);
+
+ var certificate = store.Certificates
+ .Find(X509FindType.FindByThumbprint, thumbprint, validOnly)
+ .OfType()
+ .FirstOrDefault();
+
+ return certificate;
+ }
+ finally
+ {
+ store.Close();
+ }
+ }
+
+ ///
+ public bool RemoveCertificate(string storeService, string thumbprint)
+ {
+ var store = this._serviceProvider.GetRequiredKeyedService(storeService);
+
+ try
+ {
+ store.Open(OpenFlags.ReadWrite);
+
+ X509Certificate2Collection? certificates = store.Certificates
+ .Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false);
+
+ if (certificates is null) return false;
+
+ store.RemoveRange(certificates);
+ return true;
+ }
+ catch (CryptographicException)
+ {
+ this._logger.LogError(
+ "Failed to remove certificate (\"{Thumbprint}\") from store (L='{Location}',N='{Name}').",
+ thumbprint,
+ store.Location,
+ store.Name
+ );
+
+ return false;
+ }
+ finally
+ {
+ store.Close();
+ }
+ }
+
+ #endregion
+
+ #region Helpers
+
+ private static X509KeyStorageFlags KeyStorageFlags
+ {
+ get
+ {
+ return OperatingSystem.IsMacOS()
+ ? X509KeyStorageFlags.Exportable
+ : X509KeyStorageFlags.DefaultKeySet;
+ }
}
#endregion
diff --git a/AdvancedSystems.Security/Services/CertificateStore.cs b/AdvancedSystems.Security/Services/CertificateStore.cs
index af4ba11..fab5ac7 100644
--- a/AdvancedSystems.Security/Services/CertificateStore.cs
+++ b/AdvancedSystems.Security/Services/CertificateStore.cs
@@ -5,7 +5,9 @@
namespace AdvancedSystems.Security.Services;
-///
+///
+/// Represents a service for an X.509 store, which is a physical store where certificates are persisted and managed.
+///
public sealed class CertificateStore : ICertificateStore
{
private readonly X509Store _store;
diff --git a/AdvancedSystems.Security/Services/CryptoRandomService.cs b/AdvancedSystems.Security/Services/CryptoRandomService.cs
index 737246b..bfc0e2f 100644
--- a/AdvancedSystems.Security/Services/CryptoRandomService.cs
+++ b/AdvancedSystems.Security/Services/CryptoRandomService.cs
@@ -5,7 +5,9 @@
namespace AdvancedSystems.Security.Services;
-///
+///
+/// Represents a service for performing cryptographically secure numerical operations.
+///
public sealed class CryptoRandomService : ICryptoRandomService
{
public CryptoRandomService()
diff --git a/AdvancedSystems.Security/Services/HMACService.cs b/AdvancedSystems.Security/Services/HMACService.cs
new file mode 100644
index 0000000..b371d4c
--- /dev/null
+++ b/AdvancedSystems.Security/Services/HMACService.cs
@@ -0,0 +1,22 @@
+using System;
+
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Cryptography;
+
+namespace AdvancedSystems.Security.Services;
+
+///
+/// Represents a service designed for computing hash algorithms.
+///
+public sealed class HMACService : IHMACService
+{
+ #region Methods
+
+ ///
+ public byte[] Compute(HashFunction hashFunction, ReadOnlySpan key, ReadOnlySpan buffer)
+ {
+ return HMACProvider.Compute(hashFunction, key, buffer);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Services/HashService.cs b/AdvancedSystems.Security/Services/HashService.cs
index 7e4288f..52e35f8 100644
--- a/AdvancedSystems.Security/Services/HashService.cs
+++ b/AdvancedSystems.Security/Services/HashService.cs
@@ -1,14 +1,14 @@
-using System.Security.Cryptography;
-
-using AdvancedSystems.Security.Abstractions;
-using AdvancedSystems.Security.Common;
+using AdvancedSystems.Security.Abstractions;
using AdvancedSystems.Security.Cryptography;
+using AdvancedSystems.Security.Extensions;
using Microsoft.Extensions.Logging;
namespace AdvancedSystems.Security.Services;
-///
+///
+/// Represents a service designed for computing hash algorithms.
+///
public sealed class HashService : IHashService
{
private readonly ILogger _logger;
@@ -21,42 +21,17 @@ public HashService(ILogger logger)
#region Methods
///
- public string GetMD5Hash(byte[] buffer)
- {
- this._logger.LogWarning("Computing hash with a cryptographically insecure hash algorithm (MD5).");
-
- byte[] md5 = Hash.Compute(buffer, HashAlgorithmName.MD5);
- return md5.ToString(Format.Hex);
- }
-
- ///
- public string GetSHA1Hash(byte[] buffer)
- {
- this._logger.LogWarning("Computing hash with a cryptographically insecure hash algorithm (SHA1.)");
-
- byte[] sha1 = Hash.Compute(buffer, HashAlgorithmName.SHA1);
- return sha1.ToString(Format.Hex);
- }
-
- ///
- public string GetSHA256Hash(byte[] buffer)
- {
- byte[] sha256 = Hash.Compute(buffer, HashAlgorithmName.SHA256);
- return sha256.ToString(Format.Hex);
- }
-
- ///
- public string GetSHA384Hash(byte[] buffer)
- {
- byte[] sha384 = Hash.Compute(buffer, HashAlgorithmName.SHA384);
- return sha384.ToString(Format.Hex);
- }
-
- ///
- public string GetSHA512Hash(byte[] buffer)
+ public byte[] Compute(HashFunction hashFunction, byte[] buffer)
{
- byte[] sha512 = Hash.Compute(buffer, HashAlgorithmName.SHA512);
- return sha512.ToString(Format.Hex);
+ if (hashFunction is HashFunction.MD5 or HashFunction.SHA1)
+ {
+ this._logger.LogWarning(
+ "Computing hash with a cryptographically insecure hash algorithm ({HashFunction}).",
+ hashFunction.GetName()
+ );
+ }
+
+ return HashProvider.Compute(hashFunction, buffer);
}
#endregion
diff --git a/AdvancedSystems.Security/Services/KDFService.cs b/AdvancedSystems.Security/Services/KDFService.cs
new file mode 100644
index 0000000..e445215
--- /dev/null
+++ b/AdvancedSystems.Security/Services/KDFService.cs
@@ -0,0 +1,22 @@
+using System.Diagnostics.CodeAnalysis;
+
+using AdvancedSystems.Security.Abstractions;
+using AdvancedSystems.Security.Cryptography;
+
+namespace AdvancedSystems.Security.Services;
+
+///
+/// Represents a service designed for employing key derivation functions.
+///
+public sealed class KDFService : IKDFService
+{
+ #region Methods
+
+ ///
+ public bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2)
+ {
+ return KDFProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out pbkdf2);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs
index b9aa3cd..2fb4368 100644
--- a/AdvancedSystems.Security/Services/RSACryptoService.cs
+++ b/AdvancedSystems.Security/Services/RSACryptoService.cs
@@ -12,28 +12,35 @@
namespace AdvancedSystems.Security.Services;
-///
-public sealed class RSACryptoService : IRSACryptoService
+///
+/// Represents a service for performing RSA-based asymmetric operations.
+///
+public sealed class RSACryptoService
{
private readonly ILogger _logger;
private readonly ICertificateService _certificateService;
- private readonly IOptions _options;
+ private readonly RSACryptoOptions _rsaOptions;
private bool _disposed = false;
private readonly X509Certificate2 _certificate;
private readonly RSACryptoProvider _provider;
- public RSACryptoService(ILogger logger, ICertificateService certificateService, IOptions options)
+ public RSACryptoService(ILogger logger, ICertificateService certificateService, IOptions rsaOptions)
{
this._logger = logger;
this._certificateService = certificateService;
- this._options = options;
-
- this._certificate = this._certificateService.GetConfiguredCertificate()
- ?? throw new ArgumentNullException(nameof(this._certificate));
-
- var config = this._options.Value;
- this._provider = new RSACryptoProvider(this._certificate, config.HashAlgorithmName, config.EncryptionPadding, config.SignaturePadding, config.Encoding);
+ this._rsaOptions = rsaOptions.Value;
+
+ this._certificate = this._certificateService.GetCertificate("default", this._rsaOptions.Thumbprint, validOnly: true)
+ ?? throw new ArgumentNullException();
+
+ this._provider = new RSACryptoProvider(
+ this._certificate,
+ this._rsaOptions.HashAlgorithmName,
+ this._rsaOptions.EncryptionPadding,
+ this._rsaOptions.SignaturePadding,
+ this._rsaOptions.Encoding
+ );
}
#region Properties
@@ -104,12 +111,6 @@ public void Dispose(bool disposing)
this._disposed = true;
}
- ///
- public bool IsValidMessage(string message, RSAEncryptionPadding? padding, Encoding? encoding = null)
- {
- return this._provider.IsValidMessage(message, padding, encoding);
- }
-
///
public string Encrypt(string message, Encoding? encoding = null)
{
diff --git a/development/AdvancedSystems-CA.pfx b/development/AdvancedSystems-CA.pfx
new file mode 100644
index 0000000..f7119ec
Binary files /dev/null and b/development/AdvancedSystems-CA.pfx differ
diff --git a/development/AdvancedSystems-PasswordCertificate.pem b/development/AdvancedSystems-PasswordCertificate.pem
new file mode 100644
index 0000000..9f66e75
--- /dev/null
+++ b/development/AdvancedSystems-PasswordCertificate.pem
@@ -0,0 +1,50 @@
+-----BEGIN CERTIFICATE-----
+MIIEPzCCAyegAwIBAgIIH/1pQHOtVjMwDQYJKoZIhvcNAQELBQAwdTELMAkGA1UE
+BhMCREUxDzANBgNVBAgTBkJlcmxpbjEPMA0GA1UEBxMGQmVybGluMRkwFwYDVQQK
+ExBBZHZhbmNlZCBTeXN0ZW1zMQwwCgYDVQQLEwNSbkQxGzAZBgNVBAMTEkFkdmFu
+Y2VkU3lzdGVtcy1DQTAeFw0yNTAxMTUwMDAwMDBaFw0zNDAxMTQyMzU5NTlaMHsx
+CzAJBgNVBAYTAkRFMQ8wDQYDVQQIEwZCZXJsaW4xDzANBgNVBAcTBkJlcmxpbjEZ
+MBcGA1UEChMQQWR2YW5jZWQgU3lzdGVtczEMMAoGA1UECxMDUm5EMSEwHwYDVQQD
+ExhBZHZhbmNlZFN5c3RlbXMtUGFzc3dvcmQwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQC5SdPh941DAuK5ZbmvDZejYM9/v0+KfzWPKmsELyb9//dbfI61
+SRM/DTcFAwuuVXljiClqwe16a2ekFC5zktRJX7YyP+6t6MGZ0IwK+5W9phSN4mKn
++FG5aZqqIbKTYieXIvPrX7XUytWwwu0IztG6+bwbupovvgh8s/b/CWuJnNf2bDex
+6gfYS3x3EyWlrkV7EDmtkJxOKAmrOJs3SEg53AIAHDsOclzQN5ZnfZXSmepA2cNJ
+TelwE/h5+JGlPIFs1SWkK16Ax0DY8U5i0SMQQgzdA6ROYu0cmBCmexborDUragI/
+1/nY5oel8R/iqcujvdeWBqmfS20GdUas+/15AgMBAAGjgcwwgckwDAYDVR0TAQH/
+BAIwADAdBgNVHQ4EFgQU4JGTjoq2UWRJoki04Q30F5U9KKgwHwYDVR0jBBgwFoAU
+nJ91jZo0rMIWBus0yjrFeyHpXZ4wDAYDVR0PBAUDAwfpgDATBgNVHSUEDDAKBggr
+BgEFBQcDATAjBgNVHREEHDAaghhBZHZhbmNlZFN5c3RlbXMtUGFzc3dvcmQwEQYJ
+YIZIAYb4QgEBBAQDAgZAMB4GCWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNhdGUw
+DQYJKoZIhvcNAQELBQADggEBAEOn9lMQt25m5D8PZglOr/9Q7pSHht9nGOrsZwQY
+lCyL4yQ/Y2ij2LG8/P6EqSsMGKvJwfRjwExInSyMkZrRoiIIoyBum/IUCrhv4KHo
+nF5DQePZlJF5lv1T+7AYpH/o1nYxi5zlbqeKlPPe311N3Qt+tLSgL2VjeSzdJwDn
+pI24to1h3vT8MajRMXDm6TnVEqoM/ZasjhQZPfwt8acKpvP9z4myn4GlVg8jElAz
+ceo+kWunhtrlwJ1ZBxz7enlTA7W4wqCRUaVeGpNovnGpQVgvvxgw+TQCzW7V6isv
+4izDv/AHd5yPwWtq9ScnIzaSgJeZqK8grVvkU/DYapamRkQ=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEHjCCAwagAwIBAgIIXkTDTpUTXIEwDQYJKoZIhvcNAQELBQAwdTELMAkGA1UE
+BhMCREUxDzANBgNVBAgTBkJlcmxpbjEPMA0GA1UEBxMGQmVybGluMRkwFwYDVQQK
+ExBBZHZhbmNlZCBTeXN0ZW1zMQwwCgYDVQQLEwNSbkQxGzAZBgNVBAMTEkFkdmFu
+Y2VkU3lzdGVtcy1DQTAeFw0yNTAxMTUwMDAwMDBaFw0zNTAxMTQyMzU5NTlaMHUx
+CzAJBgNVBAYTAkRFMQ8wDQYDVQQIEwZCZXJsaW4xDzANBgNVBAcTBkJlcmxpbjEZ
+MBcGA1UEChMQQWR2YW5jZWQgU3lzdGVtczEMMAoGA1UECxMDUm5EMRswGQYDVQQD
+ExJBZHZhbmNlZFN5c3RlbXMtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC0p9eUE66jSyqN4qvxsAJkZgE5/utVP3VJNyeelDEuhnhulmHmU4nnr6st
+9JWK+cwhxev84fGT46K7EaOGvtWoGRAIEmMbZFt6AMxW9+Gc4TyQmnSyE3e2pNb6
+4jF+ewyQRhPIpRnFzmVGRjb4Jpo7JGUrqXOR24QE6Rr1BV9+uS1xNRhMsil15M6F
+fKPxtflpQ9c+kiox0J+ou93V/Lepz1oN+rsVbIuEOrf2dIdWmKmYch8D9IAFq9+b
+19oZB3q0gwZ3WIkrqf0qh/Izgx2sdwJbkm0bvFBlGfpsxCFu4POo/xIVLqg0dczF
+KRRPCdMMq41IEFOrdin6LNTKurrHAgMBAAGjgbEwga4wDwYDVR0TAQH/BAUwAwEB
+/zAdBgNVHQ4EFgQUnJ91jZo0rMIWBus0yjrFeyHpXZ4wDAYDVR0PBAUDAweHgDA7
+BgNVHSUENDAyBggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcD
+BAYIKwYBBQUHAwgwEQYJYIZIAYb4QgEBBAQDAgAHMB4GCWCGSAGG+EIBDQQRFg94
+Y2EgY2VydGlmaWNhdGUwDQYJKoZIhvcNAQELBQADggEBAIRl82T96zAWWycexCcN
+z8KwleIXFwRDGWaRLWhcAj3wHB5YnJibTy2O+jbqaC11/s3OUrFvRg1emCS1FWCK
+4TtPA2pFm1wiY36RiBp5y7pdqhm6xoy5pyNb0Ub9pK0YbAF/cxu6yQ8iuQq033aW
+rzkkNNlyrHJKTep4JmVdKtuLrCEUcEE6N5DcaIvG6lpYsX4Iz5jcg95hyDAG38b7
+xFHZitAsqKMucz1PwI6wn49TOVOUCL77pwZMmwHiUzbK4Hl4iyiAFgSEoM3H0o17
+94RHschDVLDYIZo534gmwVF1iWkn5Xo62Q8hc4QXsnSREVwtmTJw+fYZy+I+HRvB
+n4g=
+-----END CERTIFICATE-----
diff --git a/development/AdvancedSystems-PrivateKey.pk8 b/development/AdvancedSystems-PrivateKey.pk8
new file mode 100644
index 0000000..fdcab7a
--- /dev/null
+++ b/development/AdvancedSystems-PrivateKey.pk8
@@ -0,0 +1,30 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQkQCzXkHN/Q7fs2Tj
+4Udh+QICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAAhhM+x1nzLWsTx
+iGot3jMEggTQ2rYdwJqpw74JqQOM40wXh84Jy6B8974mgm1s74A355BQJWtSJGiF
+/MjsJptpVC8yaLIw4EY/D9DniFw37hzeJHDnojwm4QEwR99ZhRyWsnY46akA7DjV
+9U/OIcXTE3oLPnPc2wHHmVGu3GH+GvojaZ4UpCBqICF/KW+FP16BmFOI+7PLrdck
+8ANihBLtPRcEDN7GF7Bj20Oxu24EO9qlQOl8XYqN6uZB6qLubR+TnuWypYe7ah/Q
+Fe0uTxv7O8pnplsrKoJQt6/WlCA3lkJcacWwzXa3pnSCir4GeBgCoJyl27raCuiQ
+OvSGVXO4vnu5hdYIMjZv0PF/WLBPRgXNjVyLCOQ00HQf7RMSrwaen/U2Mv9rg4S9
++LW5jc+HD2ghZkSdFXHzbu+lHOApx3lA4i+tXIepxC82lIH+5wPpJ6I2qu6EQLr5
+hImF8vcjRmkLm0KvR5Py0tT+7SvwINJQxFxJwkxfYp2ewCeiLVl7GJvvkMhZZLDj
+DhRuMH9fFtsaFlG5350gEDzMeKrFnvZMLSem+9vfZRTB63g8h5DPpmGK8jTaO4d9
+ebNv770cKHaQs+D2j5x109rFrQVBugNXbIH7JsKnvqJat2roezJOhyzDOVyM+8G8
+r5BrZ565MehrdRX31aFTKWvBhT/hrUkpH4TBAhAoTF++WVZDQdCECWEOZbQyRl3P
+LxzXoHKh8ilgbrpjUF+9tDogWC6MhTqFHcb2hbzdO5A1hIyY0o8avXvltkQe6G5r
+PnBGrqKxWfctVFo/7zkfmQFOwMa1vYyWHXE5va6StwPRDBrWy24ayh2UwI2YRTDQ
+1y8+y0edhrbquCYg9x6b4zQpNcu9YRUFv/EfRQoEUWcGj9JzZAI07G2CTIySee5l
+I4nwQbvFILTGNJ3HQVJ11AJwaLu/4B453T/TQRzmR/FI4/RgSF++Nz1VXq7cnXz6
+2N96pXE7j03HPszkoqjLs0KWUGHKLIuON9L3K66zSTiNF69TZzrdVevsj559C4eL
+FX/wIpC6TwKAozAaKN6g6NyDb24pNBMAL9d0j/D6o65dPDSOrNGwjr60QNXjvf0M
+1UaU5DP4+VQKXJIAkxpL32eD4ayrpqxblAoVQs/vwN3JWPwgTjodznt2D7hdDQvo
+mzzyvMU8XWRkGKxKjNNtyiNd6gzfoYIcMxFiCTJGuIWo49tzWE/7uRqGfX9lMf+D
+vDCeomH8zJNT2Rfqe7e1j0QG7u84+JYu91skUJ9Z0XKkMyWPDHD/75NlM+pFZcMC
+CmTsqxO729fPuI/iyRKAInyGC7uGWN2yWd+rrggZ4nNSm90kZpOyuyCI7BfLC7jg
+zXym+PES7c68uw96dQNwjJG8RsoRK17CpXN4/CXnuXlN5bvmMBVcssrNKaMWbEA7
+18SqDzxEHWw0LnWv4Pi3QrRVcZtBUqzL06vMp6t7cqa+MRCLzQXIrk89RNVfk7Cu
+zcOuBnI5VresUtplv9x+UdhzWtkpZP3ch3h/bUqVN5dPEDX7raKAwyHf2FUqlsQc
+GzjPjzWCv1AAQQqslyw8crl07t45WcjyVPUGrzavzhP+G5xoDGAKBx2mfKMVwHON
+TVDUcJbMl1JBBj3WhFbNTDy+maYDFozGp4dQK+0CA+drbQC2I9c7PeI=
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/development/about.md b/development/about.md
new file mode 100644
index 0000000..358e3bb
--- /dev/null
+++ b/development/about.md
@@ -0,0 +1,17 @@
+# About
+
+This folder contains self-signed test certificates that were created for development
+purposes only.
+
+## Certificates
+
+| Name | Thumbprint |
+|-----------------------------------------|--------------------------------------------|
+| AdvancedSystems-CA.pfx | `2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC` |
+| AdvancedSystems-PasswordCertificate.pem | `F0078AAD21DECAC0BB5FB6400ABB4198F98441A8` |
+
+## Keys
+
+| Name | Originates From |
+|-----------------------------------------|--------------------------------------------|
+| AdvancedSystems-PrivateKey.pk8 | AdvancedSystems-PasswordCertificate.pem |
diff --git a/docs/docs/providers.md b/docs/docs/providers.md
new file mode 100644
index 0000000..5046db3
--- /dev/null
+++ b/docs/docs/providers.md
@@ -0,0 +1,15 @@
+# Providers
+
+TODO
+
+## CryptoRandomProvider
+
+TODO
+
+## Hash
+
+TODO
+
+## RSACryptoProvider
+
+TODO
diff --git a/docs/docs/services.md b/docs/docs/services.md
new file mode 100644
index 0000000..1ee8e0e
--- /dev/null
+++ b/docs/docs/services.md
@@ -0,0 +1,23 @@
+# Services
+
+TODO
+
+## CertificateService
+
+TODO
+
+## CertificateStore
+
+TODO
+
+## CryptoRandomService
+
+TODO
+
+## HashService
+
+TODO
+
+## RSACryptoService
+
+TODO
diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml
index 47aa192..ec5a0c6 100644
--- a/docs/docs/toc.yml
+++ b/docs/docs/toc.yml
@@ -2,5 +2,9 @@
href: introduction.md
- name: Getting Started
href: getting-started.md
+- name: Providers
+ href: providers.md
+- name: Services
+ href: services.md
- name: Changelog
href: changelog.md
diff --git a/readme.md b/readme.md
index a7c00af..764c837 100644
--- a/readme.md
+++ b/readme.md
@@ -31,10 +31,17 @@ for debugging .NET assemblies.
## Development Environment
+Configure local user secrets for the test suite (optional):
+
+```powershell
+$Password = Read-Host -Prompt "AdvancedSystems-CA.pfx Password"
+dotnet user-secrets set CertificatePassword $Password --project ./AdvancedSystems.Tests
+```
+
Run test suite:
```powershell
-dotnet test .\AdvancedSystems.Core.Tests\ --no-logo
+dotnet test ./AdvancedSystems.Core.Tests --configuration Release
```
In addition to unit testing, this project also uses stryker for mutation testing, which is setup to be installed with
@@ -52,5 +59,5 @@ dotnet stryker
Build and serve documentation locally (`http://localhost:8080`):
```powershell
-docfx .\docs\docfx.json --serve
+docfx ./docs/docfx.json --serve
```