├── 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 | [](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 | [](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 |
--------------------------------------------------------------------------------