├── RSAKeyVaultProvider.Tests ├── private │ └── .gitkeep ├── xunit.runner.json ├── KeyVaultSigningContextTests.cs ├── RSAKeyVaultProvider.Tests.csproj ├── AzureKeyVaultSignConfigurationSet.cs ├── AzureFactAttribute.cs ├── KeyVaultConfigurationDiscoverer.cs ├── ECDsaKeyVaultProviderTests.cs └── RSAKeyVaultProviderTests.cs ├── RSAKeyVaultProvider.snk ├── .github └── dependabot.yml ├── RSAKeyVaultProvider ├── SignatureAlgorithm.cs ├── Sha1Helper.cs ├── EncryptionPaddingTranslator.cs ├── RSAKeyVaultProvider.csproj ├── SignatureAlgorithmTranslator.cs ├── ECDsaKeyVaultExtensions.cs ├── RSAKeyVaultExtensions.cs ├── KeyVaultContext.cs ├── RSAKeyVault.cs └── ECDsaKeyVault.cs ├── version.json ├── config └── signclient.json ├── Directory.Build.props ├── Directory.Build.targets ├── LICENSE ├── CodeCoverage.runsettings ├── RSAKeyVaultProvider.sln ├── .gitattributes ├── README.md ├── .editorconfig ├── azure-pipelines.yml └── .gitignore /RSAKeyVaultProvider.Tests/private/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novotnyllc/RSAKeyVaultProvider/HEAD/RSAKeyVaultProvider.snk -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider.Tests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "diagnosticMessages": true, 3 | "methodDisplay": "method", 4 | "parallelizeAssembly": true, 5 | "shadowCopy": false 6 | } 7 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider/SignatureAlgorithm.cs: -------------------------------------------------------------------------------- 1 | namespace RSAKeyVaultProvider 2 | { 3 | internal enum KeyVaultSignatureAlgorithm 4 | { 5 | RSAPkcs15, 6 | ECDsa 7 | } 8 | } -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.1", 3 | "publicReleaseRefSpec": [ 4 | "^refs/heads/master$", // we release out of master 5 | "^refs/tags/v\\d+\\.\\d+" // we also release tags starting with vN.N 6 | ], 7 | "nugetPackageVersion":{ 8 | "semVer": 2 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /config/signclient.json: -------------------------------------------------------------------------------- 1 | { 2 | "SignClient": { 3 | "AzureAd": { 4 | "AADInstance": "https://login.microsoftonline.com/", 5 | "ClientId": "6632f806-5a64-4bc6-87cf-b622b2ec62d3", 6 | "TenantId": "71048637-3782-41a3-b6b2-6f4ac8a25ae0" 7 | }, 8 | "Service": { 9 | "Url": "https://codesign.novotny.org/", 10 | "ResourceId": "https://SignService/0263d4ba-331b-46d1-85e1-bee9898a65a6" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Claire Novotny 5 | true 6 | true 7 | 8 | 9 | 10 | 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider/Sha1Helper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | 4 | namespace RSAKeyVaultProvider 5 | { 6 | static class Sha1Helper 7 | { 8 | const int SHA1_SIZE = 20; 9 | readonly static byte[] sha1Digest = new byte[] { 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A, 0x05, 0x00, 0x04, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; 10 | 11 | public static byte[] CreateDigest(byte[] hash) 12 | { 13 | if (hash.Length != SHA1_SIZE) 14 | throw new ArgumentException("Invalid hash value"); 15 | 16 | byte[] pkcs1Digest = (byte[])sha1Digest.Clone(); 17 | Array.Copy(hash, 0, pkcs1Digest, pkcs1Digest.Length - hash.Length, hash.Length); 18 | 19 | return pkcs1Digest; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider.Tests/KeyVaultSigningContextTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Security.Cryptography; 3 | using Xunit; 4 | using System; 5 | 6 | namespace RSAKeyVaultProviderTests 7 | { 8 | public class KeyVaultSigningContextTests 9 | { 10 | private readonly AzureKeyVaultSignConfigurationSet _configuration; 11 | 12 | public KeyVaultSigningContextTests() 13 | { 14 | var creds = TestAzureCredentials.Credentials; 15 | if (creds == null) 16 | { 17 | return; 18 | } 19 | _configuration = new AzureKeyVaultSignConfigurationSet 20 | { 21 | AzureClientId = creds.ClientId, 22 | AzureClientSecret = creds.ClientSecret, 23 | AzureKeyVaultUrl = new Uri(creds.AzureKeyVaultUrl), 24 | AzureKeyVaultKeyName = creds.AzureKeyVaultCertificateName, 25 | Mode = KeyVaultMode.Certificate 26 | }; 27 | } 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider/EncryptionPaddingTranslator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | 4 | using Azure.Security.KeyVault.Keys.Cryptography; 5 | 6 | namespace RSAKeyVaultProvider 7 | { 8 | static class EncryptionPaddingTranslator 9 | { 10 | public static EncryptionAlgorithm EncryptionPaddingToJwsAlgId(RSAEncryptionPadding padding) 11 | { 12 | switch (padding.Mode) 13 | { 14 | case RSAEncryptionPaddingMode.Pkcs1: 15 | return EncryptionAlgorithm.Rsa15; 16 | case RSAEncryptionPaddingMode.Oaep when padding.OaepHashAlgorithm == HashAlgorithmName.SHA1: 17 | return EncryptionAlgorithm.RsaOaep; 18 | case RSAEncryptionPaddingMode.Oaep when padding.OaepHashAlgorithm == HashAlgorithmName.SHA256: 19 | return EncryptionAlgorithm.RsaOaep256; 20 | default: 21 | throw new NotSupportedException("The padding specified is not supported."); 22 | 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2020 Claire Novotny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /RSAKeyVaultProvider.Tests/RSAKeyVaultProvider.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net47;netcoreapp2.1 5 | false 6 | RSAKeyVaultProviderTests 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider/RSAKeyVaultProvider.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Claire Novotny 6 | Claire Novotny 7 | Enables Key Vault keys and certificates to be used anywhere RSA or ECDsa is within .NET's crypto ecosystem 8 | https://raw.githubusercontent.com/novotnyllc/RSAKeyVaultProvider/master/LICENSE 9 | https://github.com/novotnyllc/RSAKeyVaultProvider 10 | Copyright (c) Claire Novotny 11 | RSA;ECDSA;Key Vault;.NET 12 | true 13 | embedded 14 | true 15 | true 16 | $(MSBuildThisFileDirectory)..\RSAKeyVaultProvider.snk 17 | true 18 | true 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /CodeCoverage.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 | .*RSAKeyVaultProvider.* 22 | 23 | 24 | .*Tests.* 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider/SignatureAlgorithmTranslator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | 4 | using Azure.Security.KeyVault.Keys.Cryptography; 5 | 6 | namespace RSAKeyVaultProvider 7 | { 8 | static class SignatureAlgorithmTranslator 9 | { 10 | public static SignatureAlgorithm SignatureAlgorithmToJwsAlgId(KeyVaultSignatureAlgorithm signatureAlgorithm, HashAlgorithmName hashAlgorithmName) 11 | { 12 | if (signatureAlgorithm == KeyVaultSignatureAlgorithm.RSAPkcs15) 13 | { 14 | if (hashAlgorithmName == HashAlgorithmName.SHA1) 15 | return new SignatureAlgorithm("RSNULL"); 16 | 17 | if (hashAlgorithmName == HashAlgorithmName.SHA256) 18 | return SignatureAlgorithm.RS256; 19 | 20 | if (hashAlgorithmName == HashAlgorithmName.SHA384) 21 | return SignatureAlgorithm.RS384; 22 | 23 | if (hashAlgorithmName == HashAlgorithmName.SHA512) 24 | return SignatureAlgorithm.RS512; 25 | } 26 | else if (signatureAlgorithm == KeyVaultSignatureAlgorithm.ECDsa) 27 | { 28 | if (hashAlgorithmName == HashAlgorithmName.SHA256) 29 | return SignatureAlgorithm.ES256; 30 | 31 | if (hashAlgorithmName == HashAlgorithmName.SHA384) 32 | return SignatureAlgorithm.ES384; 33 | 34 | if (hashAlgorithmName == HashAlgorithmName.SHA512) 35 | return SignatureAlgorithm.ES512; 36 | } 37 | 38 | throw new NotSupportedException("The algorithm specified is not supported."); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider.Tests/AzureKeyVaultSignConfigurationSet.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using System; 4 | 5 | namespace RSAKeyVaultProviderTests 6 | { 7 | public enum KeyVaultMode 8 | { 9 | Key, 10 | Certificate 11 | } 12 | public sealed class AzureKeyVaultSignConfigurationSet 13 | { 14 | public bool ManagedIdentity { get; set; } 15 | public string AzureClientId { get; set; } 16 | public string AzureClientSecret { get; set; } 17 | public string AzureTenantId { get; set; } 18 | public Uri AzureKeyVaultUrl { get; set; } 19 | public string AzureKeyVaultKeyName { get; set; } 20 | public string AzureAccessToken { get; set; } 21 | public KeyVaultMode Mode { get; set; } 22 | 23 | public bool Validate() 24 | { 25 | // Logging candidate. 26 | if (string.IsNullOrWhiteSpace(AzureAccessToken)) 27 | { 28 | if(!ManagedIdentity) 29 | { 30 | if (string.IsNullOrWhiteSpace(AzureClientId)) 31 | { 32 | return false; 33 | } 34 | if (string.IsNullOrWhiteSpace(AzureClientSecret)) 35 | { 36 | return false; 37 | } 38 | if(string.IsNullOrWhiteSpace(AzureTenantId)) 39 | { 40 | return false; 41 | } 42 | 43 | } 44 | } 45 | 46 | if (AzureKeyVaultUrl == null) 47 | { 48 | return false; 49 | } 50 | if (string.IsNullOrWhiteSpace(AzureKeyVaultKeyName)) 51 | { 52 | return false; 53 | } 54 | return true; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider.Tests/AzureFactAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | using Newtonsoft.Json; 4 | using Xunit; 5 | 6 | namespace RSAKeyVaultProviderTests 7 | { 8 | public sealed class AzureFactAttribute : FactAttribute 9 | { 10 | public AzureFactAttribute() 11 | { 12 | if (TestAzureCredentials.Credentials == null) 13 | { 14 | Skip = "Test Azure credentials are not set up correctly. " + 15 | "Please see the README for more information."; 16 | } 17 | } 18 | 19 | //Shadow the Skip as get only so it isn't set when an instance of the 20 | //attribute is declared 21 | public new string Skip { 22 | get => base.Skip; 23 | private set => base.Skip = value; 24 | } 25 | } 26 | 27 | public class TestAzureCredentials 28 | { 29 | public static TestAzureCredentials Credentials { get; } 30 | 31 | static TestAzureCredentials() 32 | { 33 | try 34 | { 35 | var basePath = Path.GetDirectoryName(typeof(TestAzureCredentials).GetTypeInfo().Assembly.Location); 36 | var credLocation = Path.Combine(basePath, @"private\azure-creds.json"); 37 | var contents = File.ReadAllText(credLocation); 38 | Credentials = JsonConvert.DeserializeObject(contents); 39 | } 40 | catch 41 | { 42 | } 43 | } 44 | 45 | public string ClientId { get; set; } 46 | public string ClientSecret { get; set; } 47 | public string TenantId { get; set; } 48 | public string AzureKeyVaultUrl { get; set; } 49 | public string AzureKeyVaultCertificateName { get; set; } 50 | public string AzureKeyVaultECDsaCertificateName { get; set; } 51 | public string AzureKeyVaultKeyName { get; set; } 52 | public string AzureKeyVaultECDsaKeyName { get; set; } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider/ECDsaKeyVaultExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Security.Cryptography.X509Certificates; 4 | 5 | using Azure.Core; 6 | using Azure.Security.KeyVault.Keys; 7 | 8 | namespace RSAKeyVaultProvider 9 | { 10 | /// 11 | /// Extensions for creating ECDsa from a Key Vault client. 12 | /// 13 | public static class ECDsaFactory 14 | { 15 | /// 16 | /// Creates an ECDsa object 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | public static ECDsa Create(TokenCredential credential, Uri keyId, JsonWebKey key) 23 | { 24 | if (credential is null) 25 | throw new ArgumentNullException(nameof(credential)); 26 | 27 | if (keyId is null) 28 | throw new ArgumentNullException(nameof(keyId)); 29 | 30 | if (key is null) 31 | throw new ArgumentNullException(nameof(key)); 32 | 33 | return new ECDsaKeyVault(new KeyVaultContext(credential, keyId, key)); 34 | } 35 | 36 | /// 37 | /// Creates an ECDsa object 38 | /// 39 | /// 40 | /// 41 | /// 42 | /// 43 | public static ECDsa Create(TokenCredential credential, Uri keyId, X509Certificate2 publicCertificate) 44 | { 45 | if (credential is null) 46 | throw new ArgumentNullException(nameof(credential)); 47 | 48 | if (keyId is null) 49 | throw new ArgumentNullException(nameof(keyId)); 50 | 51 | if (publicCertificate is null) 52 | throw new ArgumentNullException(nameof(publicCertificate)); 53 | 54 | return new ECDsaKeyVault(new KeyVaultContext(credential, keyId, publicCertificate)); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29322.22 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSAKeyVaultProvider", "RSAKeyVaultProvider\RSAKeyVaultProvider.csproj", "{4111CF9C-02B7-412C-887A-6E751D7936BF}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSAKeyVaultProvider.Tests", "RSAKeyVaultProvider.Tests\RSAKeyVaultProvider.Tests.csproj", "{E47DB183-3AF5-460C-B107-D82C5A2656A3}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9ECFD7DA-A446-4617-9E8F-B3440DA56E07}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | azure-pipelines.yml = azure-pipelines.yml 14 | README.md = README.md 15 | version.json = version.json 16 | EndProjectSection 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {4111CF9C-02B7-412C-887A-6E751D7936BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {4111CF9C-02B7-412C-887A-6E751D7936BF}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {4111CF9C-02B7-412C-887A-6E751D7936BF}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {4111CF9C-02B7-412C-887A-6E751D7936BF}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {E47DB183-3AF5-460C-B107-D82C5A2656A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {E47DB183-3AF5-460C-B107-D82C5A2656A3}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {E47DB183-3AF5-460C-B107-D82C5A2656A3}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {E47DB183-3AF5-460C-B107-D82C5A2656A3}.Release|Any CPU.Build.0 = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(SolutionProperties) = preSolution 34 | HideSolutionNode = FALSE 35 | EndGlobalSection 36 | GlobalSection(ExtensibilityGlobals) = postSolution 37 | SolutionGuid = {866BF897-9A6F-4970-B642-5D7E610C3443} 38 | EndGlobalSection 39 | EndGlobal 40 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider/RSAKeyVaultExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Security.Cryptography.X509Certificates; 4 | 5 | using Azure.Core; 6 | using Azure.Security.KeyVault.Keys; 7 | 8 | namespace RSAKeyVaultProvider 9 | { 10 | /// 11 | /// Extensions for creating RSA objects from a Key Vault client. 12 | /// 13 | public static class RSAFactory 14 | { 15 | /// 16 | /// Creates an RSA object 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | public static RSA Create(TokenCredential credential, Uri keyId, JsonWebKey key) 23 | { 24 | if (credential == null) 25 | { 26 | throw new ArgumentNullException(nameof(credential)); 27 | } 28 | 29 | if (keyId == null) 30 | { 31 | throw new ArgumentNullException(nameof(keyId)); 32 | } 33 | 34 | if (key == null) 35 | { 36 | throw new ArgumentNullException(nameof(key)); 37 | } 38 | 39 | return new RSAKeyVault(new KeyVaultContext(credential, keyId, key)); 40 | } 41 | 42 | /// 43 | /// Creates an RSA object 44 | /// 45 | /// 46 | /// 47 | /// 48 | /// 49 | public static RSA Create(TokenCredential credential, Uri keyId, X509Certificate2 publicCertificate) 50 | { 51 | if (credential == null) 52 | { 53 | throw new ArgumentNullException(nameof(credential)); 54 | } 55 | 56 | if (keyId == null) 57 | { 58 | throw new ArgumentNullException(nameof(keyId)); 59 | } 60 | 61 | if (publicCertificate == null) 62 | { 63 | throw new ArgumentNullException(nameof(publicCertificate)); 64 | } 65 | 66 | return new RSAKeyVault(new KeyVaultContext(credential, keyId, publicCertificate)); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | The `RSAKeyVaultProvider` enables you to use secrets and certificates stored in an 3 | Azure Key Vault for performing signing and decryption operations. (Encrypt and verify 4 | can be done locally with the public key material.) The type derives from `RSA` so can 5 | be used anywhere an `AsymmetricAlgorithm` can be, including with `SignedXml` types. 6 | 7 | ## Package 8 | NuGet: `RSAKeyVaultProvider`
9 | [![RSAKeyVaultProvider](https://img.shields.io/nuget/v/RSAKeyVaultProvider.svg)](https://www.nuget.org/packages/RSAKeyVaultProvider) 10 | 11 | CI feed is on Azure Artifacts: 12 | `https://pkgs.dev.azure.com/clairernovotny/GitBuilds/_packaging/RSAKeyVaultProvider/nuget/v3/index.json`
13 | [![RSAKeyVaultProvider package in RSAKeyVaultProvider feed in Azure Artifacts](https://feeds.dev.azure.com/clairernovotny/96789f1c-e804-4671-be78-d063a4eced9b/_apis/public/Packaging/Feeds/4e903115-b002-444a-9696-85e1faf90bf8/Packages/dd0c51ea-6eeb-4872-a9dc-9083718d61d1/Badge)](https://dev.azure.com/onovotny/GitBuilds/_packaging?_a=package&feed=4e903115-b002-444a-9696-85e1faf90bf8&package=dd0c51ea-6eeb-4872-a9dc-9083718d61d1&preferRelease=true) 14 | 15 | ## Setup 16 | To run these tests, you'll need to import a code signing certificate into an 17 | Azure Key Vault. You can do this by importing the PFX for certs you already have, 18 | or, the harder way, by generating a CSR in the HSM and using that for an EV Code 19 | Signing certificate. You will also need to create a new RSA key using `Add-AzureKeyVaultKey` or 20 | the UI mentioned below. Use the key name as the `azureKeyVaultKeyName` in the 21 | config and the certificate name as the `azureKeyVaultCertificateName`. 22 | 23 | You can also use the Azure Portal to generate a new key and certificate. In the cetificate make sure 24 | to go to the advanced policies and select "Data Encipherment" so that it can do the decrypt tests. 25 | 26 | Create a service principal / application and grant it access to the Key Vault with the following 27 | permissions: 28 | 29 | | Category | Permission | 30 | | ----- | ---- | 31 | | Key | Get, Sign, Decrypt | 32 | | Certificate | Get | 33 | 34 | 35 | You'll need to drop a json file called `azure-creds.json` in the tests `private` directory 36 | with the following values: 37 | 38 | ```json 39 | { 40 | "clientId": "", 41 | "clientSecret": "", 42 | "tenantId": "", 43 | "azureKeyVaultUrl": "", 44 | "azureKeyVaultCertificateName": "", 45 | "azureKeyVaultKeyName": "" 46 | } 47 | ``` 48 | 49 | ## Azure Key Vault Explorer 50 | There's a handy GUI for accessing Key Vault and includes support for importing certificates: 51 | https://github.com/elize1979/AzureKeyVaultExplorer 52 | 53 | The app defaults to logging into an @microsoft.com account, so if you want to connect to a 54 | different directory, you need to change the settings first. Change the `Authority` to `https://login.windows.net/common` 55 | and edit the `DomainHints` value to have your AAD domain(s) in it. 56 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | # From https://raw.githubusercontent.com/dotnet/roslyn/master/.editorconfig 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Don't use tabs for indentation. 8 | [*] 9 | indent_style = space 10 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 11 | 12 | # Code files 13 | [*.{cs,csx,vb,vbx}] 14 | indent_size = 4 15 | 16 | # Xml project files 17 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 18 | indent_size = 2 19 | 20 | # Xml config files 21 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 22 | indent_size = 2 23 | 24 | # JSON files 25 | [*.json] 26 | indent_size = 2 27 | 28 | # Dotnet code style settings: 29 | [*.{cs,vb}] 30 | # Sort using and Import directives with System.* appearing first 31 | dotnet_sort_system_directives_first = true 32 | # Avoid "this." and "Me." if not necessary 33 | dotnet_style_qualification_for_field = false:suggestion 34 | dotnet_style_qualification_for_property = false:suggestion 35 | dotnet_style_qualification_for_method = false:suggestion 36 | dotnet_style_qualification_for_event = false:suggestion 37 | 38 | # Use language keywords instead of framework type names for type references 39 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 40 | dotnet_style_predefined_type_for_member_access = true:suggestion 41 | 42 | # Suggest more modern language features when available 43 | dotnet_style_object_initializer = true:suggestion 44 | dotnet_style_collection_initializer = true:suggestion 45 | dotnet_style_coalesce_expression = true:suggestion 46 | dotnet_style_null_propagation = true:suggestion 47 | dotnet_style_explicit_tuple_names = true:suggestion 48 | 49 | # CSharp code style settings: 50 | [*.cs] 51 | # Prefer "var" everywhere 52 | csharp_style_var_for_built_in_types = true:suggestion 53 | csharp_style_var_when_type_is_apparent = true:suggestion 54 | csharp_style_var_elsewhere = true:suggestion 55 | 56 | # Prefer method-like constructs to have a block body 57 | csharp_style_expression_bodied_methods = false:none 58 | csharp_style_expression_bodied_constructors = false:none 59 | csharp_style_expression_bodied_operators = false:none 60 | 61 | # Prefer property-like constructs to have an expression-body 62 | csharp_style_expression_bodied_properties = true:none 63 | csharp_style_expression_bodied_indexers = true:none 64 | csharp_style_expression_bodied_accessors = true:none 65 | 66 | # Suggest more modern language features when available 67 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 68 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 69 | csharp_style_inlined_variable_declaration = true:suggestion 70 | csharp_style_throw_expression = true:suggestion 71 | csharp_style_conditional_delegate_call = true:suggestion 72 | 73 | # Newline settings 74 | csharp_new_line_before_open_brace = all 75 | csharp_new_line_before_else = true 76 | csharp_new_line_before_catch = true 77 | csharp_new_line_before_finally = true 78 | csharp_new_line_before_members_in_object_initializers = true 79 | csharp_new_line_before_members_in_anonymous_types = true 80 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | - rel/* 4 | 5 | pr: 6 | - master 7 | - rel/* 8 | 9 | variables: 10 | BuildConfiguration: Release 11 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 12 | 13 | stages: 14 | - stage: Build 15 | jobs: 16 | - job: Build 17 | pool: 18 | vmImage: windows-latest 19 | 20 | steps: 21 | - task: UseDotNet@2 22 | inputs: 23 | version: 3.1.x 24 | 25 | - task: UseDotNet@2 26 | inputs: 27 | version: 2.1.x 28 | packageType: runtime 29 | 30 | - task: DotNetCoreCLI@2 31 | inputs: 32 | command: custom 33 | custom: tool 34 | arguments: install --tool-path . nbgv 35 | displayName: Install NBGV tool 36 | 37 | - script: nbgv cloud 38 | displayName: Set Version 39 | 40 | - task: DotNetCoreCLI@2 41 | inputs: 42 | command: pack 43 | packagesToPack: RSAKeyVaultProvider/RSAKeyVaultProvider.csproj 44 | configuration: $(BuildConfiguration) 45 | packDirectory: $(Build.ArtifactStagingDirectory)\Packages 46 | verbosityPack: Minimal 47 | displayName: Pack 48 | 49 | - task: DotNetCoreCLI@2 50 | inputs: 51 | command: test 52 | projects: .\RSAKeyVaultProvider.Tests\RSAKeyVaultProvider.Tests.csproj 53 | arguments: -c $(BuildConfiguration) --collect:"Code Coverage" -s $(System.DefaultWorkingDirectory)/CodeCoverage.runsettings /p:DebugType=portable 54 | displayName: Run Tests 55 | 56 | - publish: $(Build.ArtifactStagingDirectory)\Packages 57 | displayName: Publish build packages 58 | artifact: BuildPackages 59 | 60 | - publish: config 61 | displayName: Publish signing config 62 | artifact: config 63 | 64 | - stage: CodeSign 65 | condition: and(succeeded('Build'), not(eq(variables['build.reason'], 'PullRequest'))) 66 | jobs: 67 | - deployment: CodeSign 68 | displayName: Code Signing 69 | pool: 70 | vmImage: windows-latest 71 | environment: Code Sign - CI 72 | variables: 73 | - group: Sign Client Credentials 74 | strategy: 75 | runOnce: 76 | deploy: 77 | steps: 78 | - task: DotNetCoreCLI@2 79 | inputs: 80 | command: custom 81 | custom: tool 82 | arguments: install --tool-path . SignClient 83 | displayName: Install SignTool tool 84 | 85 | - pwsh: | 86 | .\SignClient "Sign" ` 87 | --baseDirectory "$(Pipeline.Workspace)\BuildPackages" ` 88 | --input "**/*.nupkg" ` 89 | --config "$(Pipeline.Workspace)\config\signclient.json" ` 90 | --user "$(SignClientUser)" ` 91 | --secret "$(SignClientSecret)" ` 92 | --name "RSAKeyVaultProvider" ` 93 | --description "RSAKeyVaultProvider" ` 94 | --descriptionUrl "https://github.com/novotnyllc/RSAKeyVaultProvider" 95 | displayName: Sign packages 96 | 97 | - publish: $(Pipeline.Workspace)/BuildPackages 98 | displayName: Publish Signed Packages 99 | artifact: SignedPackages 100 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider.Tests/KeyVaultConfigurationDiscoverer.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | using Azure.Identity; 3 | using Azure.Security.KeyVault.Certificates; 4 | using Azure.Security.KeyVault.Keys; 5 | 6 | using RSAKeyVaultProvider; 7 | using System; 8 | using System.Security.Cryptography.X509Certificates; 9 | using System.Threading.Tasks; 10 | 11 | namespace RSAKeyVaultProviderTests 12 | { 13 | internal static class KeyVaultConfigurationDiscoverer 14 | { 15 | public static async Task Materialize(AzureKeyVaultSignConfigurationSet configuration) 16 | { 17 | TokenCredential credential = configuration.ManagedIdentity switch 18 | { 19 | true => new DefaultAzureCredential(), 20 | false => new ClientSecretCredential(configuration.AzureTenantId, configuration.AzureClientId, configuration.AzureClientSecret) 21 | }; 22 | 23 | if (configuration.Mode == KeyVaultMode.Certificate) 24 | { 25 | var certificateClient = new CertificateClient(configuration.AzureKeyVaultUrl, credential); 26 | var cert = await certificateClient.GetCertificateAsync(configuration.AzureKeyVaultKeyName).ConfigureAwait(false); 27 | 28 | var x509Certificate = new X509Certificate2(cert.Value.Cer); 29 | var keyId = cert.Value.KeyId; 30 | 31 | return new AzureKeyVaultMaterializedConfiguration(credential, keyId, publicCertificate: x509Certificate); 32 | } 33 | else if(configuration.Mode == KeyVaultMode.Key) 34 | { 35 | var keyClient = new KeyClient(configuration.AzureKeyVaultUrl, credential); 36 | var key = await keyClient.GetKeyAsync(configuration.AzureKeyVaultKeyName).ConfigureAwait(false); 37 | return new AzureKeyVaultMaterializedConfiguration(credential, key.Value.Id, key.Value.Key); 38 | } 39 | throw new ArgumentOutOfRangeException(nameof(configuration)); 40 | } 41 | } 42 | 43 | public class AzureKeyVaultMaterializedConfiguration 44 | { 45 | public AzureKeyVaultMaterializedConfiguration(TokenCredential credential, 46 | Uri keyIdentifier, 47 | JsonWebKey key = null, 48 | X509Certificate2 publicCertificate = null) 49 | { 50 | 51 | 52 | PublicCertificate = publicCertificate; 53 | TokenCredential = credential ?? throw new ArgumentNullException(nameof(credential)); 54 | KeyIdentifier = keyIdentifier ?? throw new ArgumentNullException(nameof(keyIdentifier)); 55 | if(publicCertificate == null && key == null) 56 | throw new ArgumentNullException(nameof(key), "Either key or publicCertificate must be set"); 57 | 58 | Key = key; 59 | } 60 | 61 | /// 62 | /// Can be null if Key isn't part of an x509 certificate 63 | /// 64 | public X509Certificate2 PublicCertificate { get; } 65 | 66 | public TokenCredential TokenCredential { get; } 67 | 68 | public Uri KeyIdentifier { get; } 69 | /// 70 | /// Only contains the public key 71 | /// 72 | public JsonWebKey Key { get; } 73 | 74 | public RSAKeyVault ToRSA() 75 | { 76 | if (PublicCertificate != null) 77 | { 78 | return (RSAKeyVault)RSAFactory.Create(TokenCredential, KeyIdentifier, PublicCertificate); 79 | } 80 | 81 | return (RSAKeyVault)RSAFactory.Create(TokenCredential, KeyIdentifier, Key); 82 | } 83 | 84 | public ECDsaKeyVault ToECDsa() 85 | { 86 | if (PublicCertificate != null) 87 | { 88 | return (ECDsaKeyVault)ECDsaFactory.Create(TokenCredential, KeyIdentifier, PublicCertificate); 89 | } 90 | 91 | return (ECDsaKeyVault)ECDsaFactory.Create(TokenCredential, KeyIdentifier, Key); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider/KeyVaultContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Security.Cryptography.X509Certificates; 4 | 5 | using Azure.Security.KeyVault.Keys; 6 | using Azure.Security.KeyVault.Keys.Cryptography; 7 | using Azure.Core; 8 | 9 | namespace RSAKeyVaultProvider 10 | { 11 | /// 12 | /// A signing context used for signing packages with Azure Key Vault Keys. 13 | /// 14 | public struct KeyVaultContext 15 | { 16 | readonly CryptographyClient cryptographyClient; 17 | 18 | /// 19 | /// Creates a new Key Vault context. 20 | /// 21 | public KeyVaultContext(TokenCredential credential, Uri keyId, JsonWebKey key) 22 | { 23 | KeyIdentifier = keyId ?? throw new ArgumentNullException(nameof(keyId)); 24 | Key = key ?? throw new ArgumentNullException(nameof(key)); 25 | 26 | 27 | cryptographyClient = new CryptographyClient(keyId, credential); 28 | Certificate = null; 29 | } 30 | 31 | /// 32 | /// Creates a new Key Vault context. 33 | /// 34 | public KeyVaultContext(TokenCredential credential, Uri keyId, X509Certificate2 publicCertificate) 35 | { 36 | if (credential is null) 37 | { 38 | throw new ArgumentNullException(nameof(credential)); 39 | } 40 | 41 | Certificate = publicCertificate ?? throw new ArgumentNullException(nameof(publicCertificate)); 42 | KeyIdentifier = keyId ?? throw new ArgumentNullException(nameof(keyId)); 43 | 44 | cryptographyClient = new CryptographyClient(keyId, credential); 45 | 46 | string algorithm = publicCertificate.GetKeyAlgorithm(); 47 | 48 | switch (algorithm) 49 | { 50 | case "1.2.840.113549.1.1.1": //rsa 51 | using (var rsa = publicCertificate.GetRSAPublicKey()) 52 | { 53 | Key = new JsonWebKey(rsa, includePrivateParameters: false); 54 | } 55 | break; 56 | case "1.2.840.10045.2.1": //ec 57 | using (var ecdsa = publicCertificate.GetECDsaPublicKey()) 58 | { 59 | Key = new JsonWebKey(ecdsa, includePrivateParameters: false); 60 | } 61 | break; 62 | default: 63 | throw new NotSupportedException($"Certificate algorithm '{algorithm}' is not supported."); 64 | } 65 | } 66 | 67 | /// 68 | /// Gets the certificate and public key used to validate the signature. May be null if 69 | /// Key isn't part of a certificate 70 | /// 71 | public X509Certificate2 Certificate { get; } 72 | 73 | /// 74 | /// Identifyer of current key 75 | /// 76 | public Uri KeyIdentifier { get; } 77 | 78 | /// 79 | /// Public key 80 | /// 81 | public JsonWebKey Key { get; } 82 | 83 | internal byte[] SignDigest(byte[] digest, HashAlgorithmName hashAlgorithm, KeyVaultSignatureAlgorithm signatureAlgorithm) 84 | { 85 | var algorithm = SignatureAlgorithmTranslator.SignatureAlgorithmToJwsAlgId(signatureAlgorithm, hashAlgorithm); 86 | 87 | if (hashAlgorithm == HashAlgorithmName.SHA1) 88 | { 89 | if (signatureAlgorithm != KeyVaultSignatureAlgorithm.RSAPkcs15) 90 | throw new InvalidOperationException("SHA1 algorithm is not supported for this signature algorithm."); 91 | 92 | digest = Sha1Helper.CreateDigest(digest); 93 | } 94 | 95 | var sigResult = cryptographyClient.Sign(algorithm, digest); 96 | 97 | return sigResult.Signature; 98 | } 99 | 100 | internal byte[] DecryptData(byte[] cipherText, RSAEncryptionPadding padding) 101 | { 102 | var algorithm = EncryptionPaddingTranslator.EncryptionPaddingToJwsAlgId(padding); 103 | 104 | var dataResult = cryptographyClient.Decrypt(algorithm, cipherText); 105 | return dataResult.Plaintext; 106 | } 107 | 108 | /// 109 | /// Returns true if properly constructed. If default, then false. 110 | /// 111 | public bool IsValid => cryptographyClient != null; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider/RSAKeyVault.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | 5 | namespace RSAKeyVaultProvider 6 | { 7 | /// 8 | /// RSA implementation that uses Azure Key Vault 9 | /// 10 | public sealed class RSAKeyVault : RSA 11 | { 12 | readonly KeyVaultContext context; 13 | RSA publicKey; 14 | 15 | /// 16 | /// Creates a new RSAKeyVault instance 17 | /// 18 | /// Context with parameters 19 | public RSAKeyVault(KeyVaultContext context) 20 | { 21 | if (!context.IsValid) 22 | throw new ArgumentException("Must not be the default", nameof(context)); 23 | 24 | this.context = context; 25 | publicKey = context.Key.ToRSA(); 26 | KeySizeValue = publicKey.KeySize; 27 | LegalKeySizesValue = new[] { new KeySizes(publicKey.KeySize, publicKey.KeySize, 0) }; 28 | } 29 | 30 | /// 31 | public override byte[] SignHash(byte[] hash, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding) 32 | { 33 | CheckDisposed(); 34 | 35 | // Key Vault only supports PKCSv1 padding 36 | if (padding.Mode != RSASignaturePaddingMode.Pkcs1) 37 | throw new CryptographicException("Unsupported padding mode"); 38 | 39 | try 40 | { 41 | return context.SignDigest(hash, hashAlgorithm, KeyVaultSignatureAlgorithm.RSAPkcs15); 42 | } 43 | catch (Exception e) 44 | { 45 | throw new CryptographicException("Error calling Key Vault", e); 46 | } 47 | } 48 | 49 | /// 50 | public override bool VerifyHash(byte[] hash, byte[] signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding) 51 | { 52 | CheckDisposed(); 53 | 54 | // Verify can be done locally using the public key 55 | return publicKey.VerifyHash(hash, signature, hashAlgorithm, padding); 56 | } 57 | 58 | /// 59 | protected override byte[] HashData(byte[] data, int offset, int count, HashAlgorithmName hashAlgorithm) 60 | { 61 | CheckDisposed(); 62 | 63 | using (var digestAlgorithm = Create(hashAlgorithm)) 64 | { 65 | return digestAlgorithm.ComputeHash(data, offset, count); 66 | } 67 | } 68 | 69 | /// 70 | protected override byte[] HashData(Stream data, HashAlgorithmName hashAlgorithm) 71 | { 72 | CheckDisposed(); 73 | 74 | using (var digestAlgorithm = Create(hashAlgorithm)) 75 | { 76 | return digestAlgorithm.ComputeHash(data); 77 | } 78 | } 79 | 80 | /// 81 | public override byte[] Decrypt(byte[] data, RSAEncryptionPadding padding) 82 | { 83 | CheckDisposed(); 84 | 85 | try 86 | { 87 | return context.DecryptData(data, padding); 88 | } 89 | catch (Exception e) 90 | { 91 | throw new CryptographicException("Error calling Key Vault", e); 92 | } 93 | } 94 | 95 | /// 96 | public override byte[] Encrypt(byte[] data, RSAEncryptionPadding padding) 97 | { 98 | CheckDisposed(); 99 | 100 | return publicKey.Encrypt(data, padding); 101 | } 102 | 103 | /// 104 | public override RSAParameters ExportParameters(bool includePrivateParameters) 105 | { 106 | CheckDisposed(); 107 | 108 | if (includePrivateParameters) 109 | throw new CryptographicException("Private keys cannot be exported by this provider"); 110 | 111 | 112 | return publicKey.ExportParameters(includePrivateParameters); 113 | } 114 | 115 | /// 116 | public override void ImportParameters(RSAParameters parameters) 117 | { 118 | throw new NotSupportedException(); 119 | } 120 | 121 | void CheckDisposed() 122 | { 123 | if (publicKey == null) 124 | throw new ObjectDisposedException($"{nameof(RSAKeyVault)} is disposed"); 125 | } 126 | 127 | /// 128 | protected override void Dispose(bool disposing) 129 | { 130 | if (disposing) 131 | { 132 | publicKey?.Dispose(); 133 | publicKey = null; 134 | } 135 | 136 | base.Dispose(disposing); 137 | } 138 | 139 | private static HashAlgorithm Create(HashAlgorithmName algorithm) 140 | { 141 | if (algorithm == HashAlgorithmName.SHA1) 142 | return SHA1.Create(); 143 | 144 | if (algorithm == HashAlgorithmName.SHA256) 145 | return SHA256.Create(); 146 | 147 | if (algorithm == HashAlgorithmName.SHA384) 148 | return SHA384.Create(); 149 | 150 | if (algorithm == HashAlgorithmName.SHA512) 151 | return SHA512.Create(); 152 | 153 | throw new NotSupportedException("The specified algorithm is not supported."); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | private/ 3 | !private/.gitkeep 4 | 5 | ## files generated by popular Visual Studio add-ons. 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /RSAKeyVaultProvider/ECDsaKeyVault.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | 4 | namespace RSAKeyVaultProvider 5 | { 6 | /// 7 | /// ECDsa implementation that uses Azure Key Vault 8 | /// 9 | public sealed class ECDsaKeyVault : ECDsa 10 | { 11 | readonly KeyVaultContext context; 12 | ECDsa publicKey; 13 | 14 | /// 15 | /// Creates a new ECDsaKeyVault instance 16 | /// 17 | /// Context with parameters 18 | public ECDsaKeyVault(KeyVaultContext context) 19 | { 20 | if (!context.IsValid) 21 | throw new ArgumentException("Must not be the default", nameof(context)); 22 | 23 | this.context = context; 24 | publicKey = context.Key.ToECDsa(); 25 | KeySizeValue = publicKey.KeySize; 26 | LegalKeySizesValue = new[] { new KeySizes(publicKey.KeySize, publicKey.KeySize, 0) }; 27 | } 28 | 29 | void CheckDisposed() 30 | { 31 | if (publicKey is null) 32 | throw new ObjectDisposedException($"{nameof(ECDsaKeyVault)} is disposed."); 33 | } 34 | 35 | /// 36 | protected override void Dispose(bool disposing) 37 | { 38 | if (disposing) 39 | { 40 | publicKey?.Dispose(); 41 | publicKey = null; 42 | } 43 | 44 | base.Dispose(disposing); 45 | } 46 | 47 | public override byte[] SignHash(byte[] hash) 48 | { 49 | CheckDisposed(); 50 | ValidateKeyDigestCombination(KeySize, hash.Length); 51 | 52 | // We know from ValidateKeyDigestCombination that the key size and hash size are matched up 53 | // according to RFC 7518 Sect. 3.1. 54 | if (KeySize == 256) 55 | return context.SignDigest(hash, HashAlgorithmName.SHA256, KeyVaultSignatureAlgorithm.ECDsa); 56 | if (KeySize == 384) 57 | return context.SignDigest(hash, HashAlgorithmName.SHA384, KeyVaultSignatureAlgorithm.ECDsa); 58 | if (KeySize == 521) //ES512 uses nistP521 59 | return context.SignDigest(hash, HashAlgorithmName.SHA512, KeyVaultSignatureAlgorithm.ECDsa); 60 | 61 | throw new ArgumentException("Digest length is not valid for the key size.", nameof(hash)); 62 | } 63 | 64 | protected override byte[] HashData(byte[] data, int offset, int count, HashAlgorithmName hashAlgorithm) 65 | { 66 | ValidateKeyDigestCombination(KeySize, hashAlgorithm); 67 | 68 | using (IncrementalHash hash = IncrementalHash.CreateHash(hashAlgorithm)) 69 | { 70 | hash.AppendData(data, offset, count); 71 | return hash.GetHashAndReset(); 72 | } 73 | } 74 | 75 | /// 76 | public override bool VerifyHash(byte[] hash, byte[] signature) 77 | { 78 | CheckDisposed(); 79 | ValidateKeyDigestCombination(KeySize, hash.Length); 80 | 81 | return publicKey.VerifyHash(hash, signature); 82 | } 83 | 84 | /// 85 | public override ECParameters ExportParameters(bool includePrivateParameters) 86 | { 87 | if (includePrivateParameters) 88 | throw new CryptographicException("Private keys cannot be exported by this provider"); 89 | 90 | return publicKey.ExportParameters(false); 91 | } 92 | 93 | /// 94 | public override ECParameters ExportExplicitParameters(bool includePrivateParameters) 95 | { 96 | if (includePrivateParameters) 97 | throw new CryptographicException("Private keys cannot be exported by this provider"); 98 | 99 | return publicKey.ExportExplicitParameters(false); 100 | } 101 | 102 | /// 103 | /// Importing parameters is not supported. 104 | /// 105 | public override void ImportParameters(ECParameters parameters) => 106 | throw new NotSupportedException(); 107 | 108 | /// 109 | /// Key generation is not supported. 110 | /// 111 | public override void GenerateKey(ECCurve curve) => 112 | throw new NotSupportedException(); 113 | 114 | /// 115 | public override string ToXmlString(bool includePrivateParameters) 116 | { 117 | if (includePrivateParameters) 118 | throw new CryptographicException("Private keys cannot be exported by this provider"); 119 | 120 | return publicKey.ToXmlString(false); 121 | } 122 | 123 | /// 124 | /// Importing parameters from XML is not supported. 125 | public override void FromXmlString(string xmlString) => 126 | throw new NotSupportedException(); 127 | 128 | private static void ValidateKeyDigestCombination(int keySizeBits, int digestSizeBytes) 129 | { 130 | if (keySizeBits == 256 && digestSizeBytes == 32 || 131 | keySizeBits == 384 && digestSizeBytes == 48 || 132 | keySizeBits == 521 && digestSizeBytes == 64) 133 | { 134 | return; 135 | } 136 | 137 | throw new NotSupportedException($"The key size '{keySizeBits}' is not valid for digest of size '{digestSizeBytes}' bytes."); 138 | } 139 | 140 | private static void ValidateKeyDigestCombination(int keySizeBits, HashAlgorithmName hashAlgorithmName) 141 | { 142 | if (hashAlgorithmName != HashAlgorithmName.SHA256 && 143 | hashAlgorithmName != HashAlgorithmName.SHA384 && 144 | hashAlgorithmName != HashAlgorithmName.SHA512) 145 | { 146 | throw new NotSupportedException("The specified algorithm is not supported."); 147 | } 148 | 149 | if (keySizeBits == 256 && hashAlgorithmName == HashAlgorithmName.SHA256 || 150 | keySizeBits == 384 && hashAlgorithmName == HashAlgorithmName.SHA384 || 151 | keySizeBits == 521 && hashAlgorithmName == HashAlgorithmName.SHA512) 152 | { 153 | return; 154 | } 155 | 156 | throw new NotSupportedException($"The key size '{keySizeBits}' is not valid for digest algorithm '{hashAlgorithmName}'."); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /RSAKeyVaultProvider.Tests/ECDsaKeyVaultProviderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Security.Cryptography; 4 | using Xunit; 5 | using RSAKeyVaultProvider; 6 | 7 | namespace RSAKeyVaultProviderTests 8 | { 9 | public class ECDsaKeyVaultProviderTests 10 | { 11 | private readonly AzureKeyVaultSignConfigurationSet certificateConfiguration; 12 | private readonly AzureKeyVaultSignConfigurationSet keyConfiguration; 13 | private readonly AzureKeyVaultSignConfigurationSet certificateWithMSIConfiguration; 14 | 15 | public ECDsaKeyVaultProviderTests() 16 | { 17 | var creds = TestAzureCredentials.Credentials; 18 | if (creds is null) 19 | { 20 | return; 21 | } 22 | certificateConfiguration = new AzureKeyVaultSignConfigurationSet 23 | { 24 | AzureClientId = creds.ClientId, 25 | AzureClientSecret = creds.ClientSecret, 26 | AzureTenantId = creds.TenantId, 27 | AzureKeyVaultUrl = new Uri(creds.AzureKeyVaultUrl), 28 | AzureKeyVaultKeyName = creds.AzureKeyVaultECDsaCertificateName, 29 | 30 | Mode = KeyVaultMode.Certificate 31 | }; 32 | 33 | keyConfiguration = new AzureKeyVaultSignConfigurationSet 34 | { 35 | AzureClientId = creds.ClientId, 36 | AzureClientSecret = creds.ClientSecret, 37 | AzureTenantId = creds.TenantId, 38 | AzureKeyVaultUrl = new Uri(creds.AzureKeyVaultUrl), 39 | AzureKeyVaultKeyName = creds.AzureKeyVaultECDsaKeyName, 40 | Mode = KeyVaultMode.Key 41 | }; 42 | 43 | certificateWithMSIConfiguration = new AzureKeyVaultSignConfigurationSet 44 | { 45 | ManagedIdentity = true, 46 | AzureKeyVaultUrl = new Uri(creds.AzureKeyVaultUrl), 47 | AzureKeyVaultKeyName = creds.AzureKeyVaultECDsaCertificateName, 48 | Mode = KeyVaultMode.Certificate 49 | }; 50 | } 51 | 52 | [AzureFact] 53 | public async Task ShouldRoundTripASignatureWithCertificate() 54 | { 55 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateConfiguration); 56 | 57 | using (var ecdsa = materialized.ToECDsa()) 58 | using (var sha256 = SHA256.Create()) 59 | { 60 | var data = new byte[] { 1, 2, 3 }; 61 | var digest = sha256.ComputeHash(data); 62 | var signature = ecdsa.SignHash(digest); 63 | var result = ecdsa.VerifyHash(digest, signature); 64 | Assert.True(result); 65 | } 66 | } 67 | 68 | 69 | [AzureFact] 70 | public async Task ShouldRoundTripASignatureWithCertificateViaMsi() 71 | { 72 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateWithMSIConfiguration); 73 | 74 | using (var ecdsa = materialized.ToECDsa()) 75 | using (var sha256 = SHA256.Create()) 76 | { 77 | var data = new byte[] { 1, 2, 3 }; 78 | var digest = sha256.ComputeHash(data); 79 | var signature = ecdsa.SignHash(digest); 80 | var result = ecdsa.VerifyHash(digest, signature); 81 | Assert.True(result); 82 | } 83 | } 84 | 85 | [AzureFact] 86 | public async Task ShouldFailToVerifyBadSignatureWithCertificate() 87 | { 88 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateConfiguration); 89 | using (var ecdsa = materialized.ToECDsa()) 90 | using (var sha256 = SHA256.Create()) 91 | { 92 | var data = new byte[] { 1, 2, 3 }; 93 | var digest = sha256.ComputeHash(data); 94 | var signature = ecdsa.SignHash(digest); 95 | signature[0] = (byte)~signature[0]; //Flip some bits. 96 | var result = ecdsa.VerifyHash(digest, signature); 97 | Assert.False(result); 98 | } 99 | } 100 | 101 | [AzureFact] 102 | public async Task ShouldHashDataAndVerifyWithCertificate() 103 | { 104 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateConfiguration); 105 | using (var ecdsa = materialized.ToECDsa()) 106 | { 107 | var data = new byte[] { 1, 2, 3 }; 108 | 109 | var signature = ecdsa.SignData(data, HashAlgorithmName.SHA256); 110 | var result = ecdsa.VerifyData(data, signature, HashAlgorithmName.SHA256); 111 | Assert.True(result); 112 | } 113 | } 114 | 115 | [AzureFact] 116 | public async Task ShouldRoundTripASignatureWithKey() 117 | { 118 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(keyConfiguration); 119 | using (var ecdsa = materialized.ToECDsa()) 120 | using (var sha256 = SHA256.Create()) 121 | { 122 | var data = new byte[] { 1, 2, 3 }; 123 | var digest = sha256.ComputeHash(data); 124 | var signature = ecdsa.SignHash(digest); 125 | var result = ecdsa.VerifyHash(digest, signature); 126 | Assert.True(result); 127 | } 128 | } 129 | 130 | [AzureFact] 131 | public async Task ShouldFailToVerifyBadSignatureWithKey() 132 | { 133 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(keyConfiguration); 134 | using (var ecdsa = materialized.ToECDsa()) 135 | using (var sha256 = SHA256.Create()) 136 | { 137 | var data = new byte[] { 1, 2, 3 }; 138 | var digest = sha256.ComputeHash(data); 139 | var signature = ecdsa.SignHash(digest); 140 | signature[0] = (byte)~signature[0]; //Flip some bits. 141 | var result = ecdsa.VerifyHash(digest, signature); 142 | Assert.False(result); 143 | } 144 | } 145 | 146 | [AzureFact] 147 | public async Task ShouldHashDataAndVerifyWithKey() 148 | { 149 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(keyConfiguration); 150 | using (var ecdsa = materialized.ToECDsa()) 151 | { 152 | var data = new byte[] { 1, 2, 3 }; 153 | 154 | var signature = ecdsa.SignData(data, HashAlgorithmName.SHA256); 155 | var result = ecdsa.VerifyData(data, signature, HashAlgorithmName.SHA256); 156 | Assert.True(result); 157 | } 158 | } 159 | 160 | [AzureFact] 161 | public async Task SignDataShouldThrowForUnsupportedHashAlgorithm() 162 | { 163 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateConfiguration); 164 | using (var ecdsa = materialized.ToECDsa()) 165 | { 166 | var exception = Assert.Throws(() => 167 | ecdsa.SignData(Array.Empty(), new HashAlgorithmName("unsupported"))); 168 | 169 | Assert.Equal("The specified algorithm is not supported.", exception.Message); 170 | } 171 | } 172 | 173 | [AzureFact] 174 | public async Task SignHashShouldThrowForDigestAndKeySizeMismatch() 175 | { 176 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(keyConfiguration); 177 | using (var ecdsa = materialized.ToECDsa()) 178 | using (var sha384 = SHA384.Create()) 179 | { 180 | Assert.Equal(256, ecdsa.KeySize); 181 | 182 | var data = new byte[] { 1, 2, 3 }; 183 | var digest = sha384.ComputeHash(data); 184 | var ex = Assert.Throws(() => ecdsa.SignHash(digest)); 185 | Assert.Equal("The key size '256' is not valid for digest of size '48' bytes.", ex.Message); 186 | } 187 | } 188 | 189 | [AzureFact] 190 | public async Task SignDataShouldThrowForDigestAndKeySizeMismatch() 191 | { 192 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(keyConfiguration); 193 | using (var ecdsa = materialized.ToECDsa()) 194 | { 195 | Assert.Equal(256, ecdsa.KeySize); 196 | 197 | var data = new byte[] { 1, 2, 3 }; 198 | var ex = Assert.Throws(() => ecdsa.SignData(data, HashAlgorithmName.SHA384)); 199 | Assert.Equal("The key size '256' is not valid for digest algorithm 'SHA384'.", ex.Message); 200 | } 201 | } 202 | 203 | [AzureFact] 204 | public async Task VerifyDataShouldThrowForUnsupportedHashAlgorithm() 205 | { 206 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateConfiguration); 207 | using (var ecdsa = materialized.ToECDsa()) 208 | { 209 | var exception = Assert.Throws(() => 210 | ecdsa.VerifyData(Array.Empty(), Array.Empty(), 211 | new HashAlgorithmName("unsupported"))); 212 | 213 | Assert.Equal("The specified algorithm is not supported.", exception.Message); 214 | } 215 | } 216 | 217 | [Fact] 218 | public void DefaultContextShouldThrow() 219 | { 220 | Assert.Throws(() => new ECDsaKeyVault(default(KeyVaultContext))); 221 | } 222 | } 223 | } -------------------------------------------------------------------------------- /RSAKeyVaultProvider.Tests/RSAKeyVaultProviderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | using Xunit; 6 | using RSAKeyVaultProvider; 7 | 8 | namespace RSAKeyVaultProviderTests 9 | { 10 | public class RSAKeyVaultProviderTests 11 | { 12 | private readonly AzureKeyVaultSignConfigurationSet certificateConfiguration; 13 | private readonly AzureKeyVaultSignConfigurationSet keyConfiguration; 14 | private readonly AzureKeyVaultSignConfigurationSet certificateWithMSIConfiguration; 15 | 16 | public RSAKeyVaultProviderTests() 17 | { 18 | var creds = TestAzureCredentials.Credentials; 19 | if (creds == null) 20 | { 21 | return; 22 | } 23 | certificateConfiguration = new AzureKeyVaultSignConfigurationSet 24 | { 25 | AzureClientId = creds.ClientId, 26 | AzureClientSecret = creds.ClientSecret, 27 | AzureTenantId = creds.TenantId, 28 | AzureKeyVaultUrl = new Uri(creds.AzureKeyVaultUrl), 29 | AzureKeyVaultKeyName = creds.AzureKeyVaultCertificateName, 30 | Mode = KeyVaultMode.Certificate 31 | }; 32 | 33 | keyConfiguration = new AzureKeyVaultSignConfigurationSet 34 | { 35 | AzureClientId = creds.ClientId, 36 | AzureClientSecret = creds.ClientSecret, 37 | AzureTenantId = creds.TenantId, 38 | AzureKeyVaultUrl = new Uri(creds.AzureKeyVaultUrl), 39 | AzureKeyVaultKeyName = creds.AzureKeyVaultKeyName, 40 | Mode = KeyVaultMode.Key 41 | }; 42 | 43 | certificateWithMSIConfiguration = new AzureKeyVaultSignConfigurationSet 44 | { 45 | ManagedIdentity = true, 46 | AzureKeyVaultUrl = new Uri(creds.AzureKeyVaultUrl), 47 | AzureKeyVaultKeyName = creds.AzureKeyVaultCertificateName, 48 | Mode = KeyVaultMode.Certificate 49 | }; 50 | } 51 | 52 | [AzureFact] 53 | public async Task ShouldRoundTripASignatureWithCertificate() 54 | { 55 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateConfiguration); 56 | 57 | using (var rsa = materialized.ToRSA()) 58 | using (var sha256 = SHA256.Create()) 59 | { 60 | var data = new byte[] { 1, 2, 3 }; 61 | var digest = sha256.ComputeHash(data); 62 | var signature = rsa.SignHash(digest, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 63 | var result = rsa.VerifyHash(digest, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 64 | Assert.True(result); 65 | } 66 | } 67 | 68 | 69 | [AzureFact] 70 | public async Task ShouldRoundTripASignatureWithCertificateViaMsi() 71 | { 72 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateWithMSIConfiguration); 73 | 74 | using (var rsa = materialized.ToRSA()) 75 | using (var sha256 = SHA256.Create()) 76 | { 77 | var data = new byte[] { 1, 2, 3 }; 78 | var digest = sha256.ComputeHash(data); 79 | var signature = rsa.SignHash(digest, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 80 | var result = rsa.VerifyHash(digest, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 81 | Assert.True(result); 82 | } 83 | } 84 | 85 | [AzureFact] 86 | public async Task ShouldFailToVerifyBadSignatureWithCertificate() 87 | { 88 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateConfiguration); 89 | using (var rsa = materialized.ToRSA()) 90 | using (var sha256 = SHA256.Create()) 91 | { 92 | var data = new byte[] { 1, 2, 3 }; 93 | var digest = sha256.ComputeHash(data); 94 | var signature = rsa.SignHash(digest, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 95 | signature[0] = (byte)~signature[0]; //Flip some bits. 96 | var result = rsa.VerifyHash(digest, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 97 | Assert.False(result); 98 | } 99 | 100 | 101 | } 102 | 103 | [AzureFact] 104 | public async Task ShouldHashDataAndVerifyWithCertificate() 105 | { 106 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateConfiguration); 107 | using (var rsa = materialized.ToRSA()) 108 | { 109 | var data = new byte[] { 1, 2, 3 }; 110 | 111 | var signature = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 112 | var result = rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 113 | Assert.True(result); 114 | } 115 | 116 | } 117 | 118 | [AzureFact] 119 | public async Task ShouldRoundTripEncryptAndDecryptWithCertificate() 120 | { 121 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateConfiguration); 122 | using (var rsa = materialized.ToRSA()) 123 | { 124 | var data = Encoding.UTF8.GetBytes("Clear text"); 125 | var cipherText = rsa.Encrypt(data, RSAEncryptionPadding.Pkcs1); 126 | var returnedData = rsa.Decrypt(cipherText, RSAEncryptionPadding.Pkcs1); 127 | var text = Encoding.UTF8.GetString(returnedData); 128 | 129 | Assert.Equal("Clear text", text); 130 | } 131 | 132 | } 133 | 134 | [AzureFact] 135 | public async Task ShouldRoundTripEncryptAndDecryptWithKey() 136 | { 137 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(keyConfiguration); 138 | using (var rsa = materialized.ToRSA()) 139 | { 140 | var data = Encoding.UTF8.GetBytes("Clear text"); 141 | var cipherText = rsa.Encrypt(data, RSAEncryptionPadding.Pkcs1); 142 | var returnedData = rsa.Decrypt(cipherText, RSAEncryptionPadding.Pkcs1); 143 | var text = Encoding.UTF8.GetString(returnedData); 144 | 145 | Assert.Equal("Clear text", text); 146 | } 147 | 148 | } 149 | 150 | [AzureFact] 151 | public async Task ShouldRoundTripASignatureWithKey() 152 | { 153 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(keyConfiguration); 154 | using (var rsa = materialized.ToRSA()) 155 | using (var sha256 = SHA256.Create()) 156 | { 157 | var data = new byte[] { 1, 2, 3 }; 158 | var digest = sha256.ComputeHash(data); 159 | var signature = rsa.SignHash(digest, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 160 | var result = rsa.VerifyHash(digest, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 161 | Assert.True(result); 162 | } 163 | 164 | } 165 | 166 | [AzureFact] 167 | public async Task ShouldFailToVerifyBadSignatureWithKey() 168 | { 169 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(keyConfiguration); 170 | using (var rsa = materialized.ToRSA()) 171 | using (var sha256 = SHA256.Create()) 172 | { 173 | var data = new byte[] { 1, 2, 3 }; 174 | var digest = sha256.ComputeHash(data); 175 | var signature = rsa.SignHash(digest, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 176 | signature[0] = (byte)~signature[0]; //Flip some bits. 177 | var result = rsa.VerifyHash(digest, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 178 | Assert.False(result); 179 | } 180 | 181 | 182 | } 183 | 184 | [AzureFact] 185 | public async Task ShouldHashDataAndVerifyWithKey() 186 | { 187 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(keyConfiguration); 188 | using (var rsa = materialized.ToRSA()) 189 | { 190 | var data = new byte[] { 1, 2, 3 }; 191 | 192 | var signature = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 193 | var result = rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 194 | Assert.True(result); 195 | } 196 | 197 | } 198 | 199 | [AzureFact] 200 | public async Task SignDataShouldThrowForUnsupportedHashAlgorithm() 201 | { 202 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateConfiguration); 203 | using (var rsa = materialized.ToRSA()) 204 | { 205 | var exception = Assert.Throws(() => 206 | rsa.SignData(Array.Empty(), new HashAlgorithmName("unsupported"), RSASignaturePadding.Pkcs1)); 207 | 208 | Assert.Equal("The specified algorithm is not supported.", exception.Message); 209 | } 210 | 211 | } 212 | 213 | [AzureFact] 214 | public async Task VerifyDataShouldThrowForUnsupportedHashAlgorithm() 215 | { 216 | var materialized = await KeyVaultConfigurationDiscoverer.Materialize(certificateConfiguration); 217 | using (var rsa = materialized.ToRSA()) 218 | { 219 | var exception = Assert.Throws(() => 220 | rsa.VerifyData(Array.Empty(), Array.Empty(), 221 | new HashAlgorithmName("unsupported"), RSASignaturePadding.Pkcs1)); 222 | 223 | Assert.Equal("The specified algorithm is not supported.", exception.Message); 224 | } 225 | 226 | } 227 | 228 | [Fact] 229 | public void DefaultContextShouldThrow() 230 | { 231 | Assert.Throws(() => new RSAKeyVault(default(KeyVaultContext))); 232 | } 233 | } 234 | } 235 | --------------------------------------------------------------------------------