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 ```