-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add getcert sample for Key Vault (#14971)
Resolves part of #12742
- Loading branch information
Showing
4 changed files
with
314 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using System; | ||
using System.CommandLine; | ||
using System.CommandLine.Invocation; | ||
using System.CommandLine.IO; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Security.Cryptography; | ||
using System.Security.Cryptography.X509Certificates; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Azure.Identity; | ||
using Azure.Security.KeyVault.Certificates; | ||
using Azure.Security.KeyVault.Secrets; | ||
|
||
internal static class Program | ||
{ | ||
private static async Task<int> RunAsync( | ||
Uri vaultUri, | ||
string certificateName, | ||
string message, | ||
IConsole console) | ||
{ | ||
CancellationToken cancellationToken = s_cancellationTokenSource.Token; | ||
|
||
// Allow only credentials appropriate for this interactive tool sample. | ||
DefaultAzureCredential credential = new DefaultAzureCredential( | ||
new DefaultAzureCredentialOptions | ||
{ | ||
ExcludeEnvironmentCredential = true, | ||
ExcludeManagedIdentityCredential = true, | ||
}); | ||
|
||
// Get the certificate to use for encrypting and decrypting. | ||
CertificateClient certificateClient = new CertificateClient(vaultUri, credential); | ||
KeyVaultCertificateWithPolicy certificate = await certificateClient.GetCertificateAsync(certificateName, cancellationToken: cancellationToken); | ||
|
||
// Make sure the private key is exportable. | ||
if (certificate.Policy?.Exportable != true) | ||
{ | ||
console.Error.WriteLine($@"Error: certificate ""{certificateName}"" is not exportable."); | ||
return 1; | ||
} | ||
else if (certificate.Policy?.KeyType != CertificateKeyType.Rsa) | ||
{ | ||
console.Error.WriteLine($@"Error: certificate type ""{certificate.Policy?.KeyType}"" cannot be used to locally encrypt and decrypt."); | ||
return 1; | ||
} | ||
|
||
// Get the managed secret which contains the public and private key (if exportable). | ||
string secretName = ParseSecretName(certificate.SecretId); | ||
|
||
SecretClient secretClient = new SecretClient(vaultUri, credential); | ||
KeyVaultSecret secret = await secretClient.GetSecretAsync(secretName, cancellationToken: cancellationToken); | ||
|
||
// Get a certificate pair from the secret value. | ||
X509Certificate2 pfx = ParseCertificate(secret); | ||
|
||
// Decode and encrypt the message. | ||
byte[] plaintext = Encoding.UTF8.GetBytes(message); | ||
|
||
using RSA encryptor = (RSA)pfx.PublicKey.Key; | ||
byte[] ciphertext = encryptor.Encrypt(plaintext, RSAEncryptionPadding.OaepSHA256); | ||
|
||
console.Out.WriteLine($"Encrypted message: {Convert.ToBase64String(ciphertext)}"); | ||
|
||
// Decrypt and encode the message. | ||
using RSA decryptor = (RSA)pfx.PrivateKey; | ||
plaintext = decryptor.Decrypt(ciphertext, RSAEncryptionPadding.OaepSHA256); | ||
|
||
message = Encoding.UTF8.GetString(plaintext); | ||
console.Out.WriteLine($"Decrypted message: {message}"); | ||
|
||
return 0; | ||
} | ||
|
||
private static string ParseSecretName(Uri secretId) | ||
{ | ||
if (secretId.Segments.Length < 3) | ||
{ | ||
throw new InvalidOperationException($@"The secret ""{secretId}"" does not contain a valid name."); | ||
} | ||
|
||
return secretId.Segments[2].TrimEnd('/'); | ||
} | ||
|
||
private static X509Certificate2 ParseCertificate(KeyVaultSecret secret) | ||
{ | ||
if (string.Equals(secret.Properties.ContentType, CertificateContentType.Pkcs12.ToString(), StringComparison.InvariantCultureIgnoreCase)) | ||
{ | ||
byte[] pfx = Convert.FromBase64String(secret.Value); | ||
return new X509Certificate2(pfx); | ||
} | ||
|
||
// For PEM, you'll need to extract the base64-encoded message body. | ||
// .NET 5.0 introduces the System.Security.Cryptography.PemEncoding class to make this easier. | ||
if (string.Equals(secret.Properties.ContentType, CertificateContentType.Pem.ToString(), StringComparison.InvariantCultureIgnoreCase)) | ||
{ | ||
StringBuilder privateKeyBuilder = new StringBuilder(); | ||
StringBuilder publicKeyBuilder = new StringBuilder(); | ||
|
||
using StringReader reader = new StringReader(secret.Value); | ||
StringBuilder currentKeyBuilder = null; | ||
|
||
string line = reader.ReadLine(); | ||
while (line != null) | ||
{ | ||
if (line.Equals("-----BEGIN PRIVATE KEY-----", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
currentKeyBuilder = privateKeyBuilder; | ||
} | ||
else if (line.Equals("-----BEGIN CERTIFICATE-----", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
currentKeyBuilder = publicKeyBuilder; | ||
} | ||
else if (line.StartsWith("-----", StringComparison.Ordinal)) | ||
{ | ||
currentKeyBuilder = null; | ||
} | ||
else if (currentKeyBuilder is null) | ||
{ | ||
throw new InvalidOperationException("Invalid PEM-encoded certificate."); | ||
} | ||
else | ||
{ | ||
currentKeyBuilder.Append(line); | ||
} | ||
|
||
line = reader.ReadLine(); | ||
} | ||
|
||
string privateKeyBase64 = privateKeyBuilder?.ToString() ?? throw new InvalidOperationException("No private key found in certificate."); | ||
string publicKeyBase64 = publicKeyBuilder?.ToString() ?? throw new InvalidOperationException("No public key found in certificate."); | ||
|
||
byte[] privateKey = Convert.FromBase64String(privateKeyBase64); | ||
byte[] publicKey = Convert.FromBase64String(publicKeyBase64); | ||
|
||
X509Certificate2 certificate = new X509Certificate2(publicKey); | ||
|
||
using RSA rsa = RSA.Create(); | ||
rsa.ImportPkcs8PrivateKey(privateKey, out _); | ||
|
||
return certificate.CopyWithPrivateKey(rsa); | ||
} | ||
|
||
throw new NotSupportedException($@"Certificate encoding ""{secret.Properties.ContentType}"" is not supported."); | ||
} | ||
|
||
#region Configuration | ||
// Expose RootCommand for invoking directly via optional tests. | ||
internal static readonly RootCommand Command; | ||
private static readonly CancellationTokenSource s_cancellationTokenSource; | ||
|
||
static Program() | ||
{ | ||
Command = new RootCommand("Encrypts and decrypts a message using a certificate from Azure Key Vault") | ||
{ | ||
new Option<Uri>( | ||
alias: "--vault-name", | ||
description: "Key Vault name or URI, e.g. my-vault or https://my-vault-vault.azure.net", | ||
parseArgument: result => | ||
{ | ||
string value = result.Tokens.Single().Value; | ||
if (Uri.TryCreate(value, UriKind.Absolute, out Uri vaultUri) || | ||
Uri.TryCreate($"https://{value}.vault.azure.net", UriKind.Absolute, out vaultUri)) | ||
{ | ||
return vaultUri; | ||
} | ||
|
||
result.ErrorMessage = "Must specify a vault name or URI"; | ||
return null!; | ||
} | ||
) | ||
{ | ||
Name = "vaultUri", | ||
IsRequired = true, | ||
}, | ||
|
||
new Option<string>( | ||
aliases: new[] { "-n", "--certificate-name" }, | ||
description: "Name of the certificate to use for encrypting and decrypting." | ||
) | ||
{ | ||
IsRequired = true, | ||
}, | ||
|
||
new Option<string>( | ||
aliases: new[] { "-m", "--message" }, | ||
description: "The message to encrypt and decrypt." | ||
), | ||
}; | ||
|
||
Command.Handler = CommandHandler.Create<Uri, string, string, IConsole>(RunAsync); | ||
|
||
s_cancellationTokenSource = new CancellationTokenSource(); | ||
} | ||
|
||
private static async Task<int> Main(string[] args) | ||
{ | ||
Console.CancelKeyPress += (_, args) => | ||
{ | ||
Console.Error.WriteLine("Canceling..."); | ||
s_cancellationTokenSource.Cancel(); | ||
|
||
args.Cancel = true; | ||
}; | ||
|
||
return await Command.InvokeAsync(args); | ||
} | ||
#endregion | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
--- | ||
page_type: sample | ||
languages: | ||
- csharp | ||
products: | ||
- azure | ||
- azure-key-vault | ||
urlFragment: get-certificate-private-key | ||
name: Get a Certificate Including the Private Key | ||
description: Gets a full certificate including the private key from Azure Key Vault. | ||
--- | ||
|
||
# Get a Certificate Including the Private Key | ||
|
||
[Azure Key Vault certificates][azure-keyvault-certificates] are a great way to manage certificates. They allow you to set policies, automatically renew near-expiring certificates, and permit cryptographic operations with access to the private key. There are times, however, when you may want to download and use the entire certificate - including the private key - locally. You might have a legacy application, for example, that needs access to a key pair. | ||
|
||
> [!NOTE] | ||
> We recommend you keep cryptographic operations using the private key - including decryption, signing, and unwrapping - in Key Vault to minimize access to the private and mitigate possible breaches with a properly secured Key Vault. | ||
Key Vault stores the public key as a managed key but the entire key pair including the private key - if created or imported as exportable - as a [secret][azure-keyvault-secrets]. This example shows you how download the key pair and uses it to encrypt and decrypt a plain text message. | ||
|
||
## Getting Started | ||
|
||
This sample requires creating a certificate with an exportable private key. You'll also need to download and install the [Azure CLI](https://aka.ms/azure-cli). | ||
|
||
1. Log into Azure using the CLI: | ||
|
||
```bash | ||
az login | ||
``` | ||
|
||
2. Create a Key Vault if you haven't already: | ||
|
||
```bash | ||
az keyvault create -n <KeyVaultName> -g <ResourceGroupName> -l <Location> | ||
``` | ||
|
||
3. Create a certificate policy. You can get the default policy for a self-signed certificate as shown below: | ||
|
||
> [!NOTE] | ||
> Saving program output to a variable may vary depending on your shell. | ||
```bash | ||
p=$(az keyvault certificate get-default-policy) | ||
echo $p | ||
``` | ||
|
||
4. Create a certificate using that policy: | ||
|
||
```bash | ||
az keyvault certificate create --vault-name <KeyVaultName> -n <CertificateName> -p "$p" | ||
``` | ||
|
||
## Building the Sample | ||
|
||
To build the sample: | ||
|
||
1. Install [.NET Core 3.1](https://dot.net) or newer. | ||
|
||
2. Run in the project directory: | ||
|
||
```bash | ||
dotnet build | ||
``` | ||
|
||
## Running the Sample | ||
|
||
You can either run the executable you just build, or build and run the project at the same time: | ||
|
||
```bash | ||
dotnet run -- --vault-name <KeyVaultName> -n <CertificateName> -m "Message you want to encrypt and decrypt" | ||
``` | ||
|
||
The sample will get information about the specified certificate, download the key pair as a secret, then encrypt and decrypt your message as a test. | ||
|
||
## Links | ||
|
||
- [About Azure Key Vault certificates][azure-keyvault-certificates] | ||
- [About Azure Key Vault secrets][azure-keyvault-secrets] | ||
- [Azure Key Vault samples](https://aka.ms/azsdk/net/keyvault/samples) | ||
|
||
[azure-keyvault-certificates]: https://docs.microsoft.com/azure/key-vault/certificates/about-certificates | ||
[azure-keyvault-secrets]: https://docs.microsoft.com/azure/key-vault/secrets/about-secrets |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>netcoreapp3.1</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Azure.Identity" Version="1.2.2" /> | ||
<PackageReference Include="Azure.Security.KeyVault.Certificates" Version="4.1.0" /> | ||
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.1.0" /> | ||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20371.2" /> | ||
</ItemGroup> | ||
|
||
</Project> |