From 07221169662fc8e2af4b6ca59a5d25fe9b625760 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Tue, 8 Sep 2020 11:17:13 -0700 Subject: [PATCH] Add getcert sample for Key Vault (#14971) Resolves part of #12742 --- sdk/keyvault/samples/Directory.Build.props | 2 + sdk/keyvault/samples/getcert/Program.cs | 214 ++++++++++++++++++++ sdk/keyvault/samples/getcert/README.md | 83 ++++++++ sdk/keyvault/samples/getcert/getcert.csproj | 15 ++ 4 files changed, 314 insertions(+) create mode 100644 sdk/keyvault/samples/getcert/Program.cs create mode 100644 sdk/keyvault/samples/getcert/README.md create mode 100644 sdk/keyvault/samples/getcert/getcert.csproj diff --git a/sdk/keyvault/samples/Directory.Build.props b/sdk/keyvault/samples/Directory.Build.props index 127147fd3f153..aac01a03d888a 100644 --- a/sdk/keyvault/samples/Directory.Build.props +++ b/sdk/keyvault/samples/Directory.Build.props @@ -2,6 +2,8 @@ true + false + false diff --git a/sdk/keyvault/samples/getcert/Program.cs b/sdk/keyvault/samples/getcert/Program.cs new file mode 100644 index 0000000000000..0d88d633112fd --- /dev/null +++ b/sdk/keyvault/samples/getcert/Program.cs @@ -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 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( + 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( + aliases: new[] { "-n", "--certificate-name" }, + description: "Name of the certificate to use for encrypting and decrypting." + ) + { + IsRequired = true, + }, + + new Option( + aliases: new[] { "-m", "--message" }, + description: "The message to encrypt and decrypt." + ), + }; + + Command.Handler = CommandHandler.Create(RunAsync); + + s_cancellationTokenSource = new CancellationTokenSource(); + } + + private static async Task Main(string[] args) + { + Console.CancelKeyPress += (_, args) => + { + Console.Error.WriteLine("Canceling..."); + s_cancellationTokenSource.Cancel(); + + args.Cancel = true; + }; + + return await Command.InvokeAsync(args); + } + #endregion +} diff --git a/sdk/keyvault/samples/getcert/README.md b/sdk/keyvault/samples/getcert/README.md new file mode 100644 index 0000000000000..4c46163ec99d6 --- /dev/null +++ b/sdk/keyvault/samples/getcert/README.md @@ -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 -g -l + ``` + +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 -n -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 -n -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 diff --git a/sdk/keyvault/samples/getcert/getcert.csproj b/sdk/keyvault/samples/getcert/getcert.csproj new file mode 100644 index 0000000000000..77f2c44edeed1 --- /dev/null +++ b/sdk/keyvault/samples/getcert/getcert.csproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + +