Skip to content

Commit

Permalink
Add getcert sample for Key Vault (#14971)
Browse files Browse the repository at this point in the history
Resolves part of #12742
  • Loading branch information
heaths authored Sep 8, 2020
1 parent 235d2fe commit 0722116
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 0 deletions.
2 changes: 2 additions & 0 deletions sdk/keyvault/samples/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<PropertyGroup>
<!-- Signal that samples are building in the repo as opposed to a standalone download from Samples Browser -->
<IsSample>true</IsSample>
<IsPackable>false</IsPackable>
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
</PropertyGroup>

<Import Project="..\Directory.Build.props" />
Expand Down
214 changes: 214 additions & 0 deletions sdk/keyvault/samples/getcert/Program.cs
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
}
83 changes: 83 additions & 0 deletions sdk/keyvault/samples/getcert/README.md
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
15 changes: 15 additions & 0 deletions sdk/keyvault/samples/getcert/getcert.csproj
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>

0 comments on commit 0722116

Please sign in to comment.