Skip to content

Commit

Permalink
Implement secure key release
Browse files Browse the repository at this point in the history
Resolves Azure#14892 sans tests; see Azure#16789 and Azure#16792
  • Loading branch information
heaths committed Nov 10, 2020
1 parent 5dc9e36 commit 7865506
Show file tree
Hide file tree
Showing 14 changed files with 451 additions and 1 deletion.
10 changes: 10 additions & 0 deletions sdk/keyvault/Azure.Security.KeyVault.Keys/src/CreateKeyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ public CreateKeyOptions()
/// </summary>
public bool? Enabled { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the private key can be exported.
/// </summary>
public bool? Exportable { get; set; }

/// <summary>
/// Gets or sets the policy rules under which the key can be exported.
/// </summary>
public KeyReleasePolicy ReleasePolicy { get; set; }

/// <summary>
/// Gets a dictionary of tags with specific metadata about the key. Although this collection cannot be set, it can be modified
/// or initialized with a <see href="https://docs.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/how-to-initialize-a-dictionary-with-a-collection-initializer">collection initializer</see>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public class CreateRsaKeyOptions : CreateKeyOptions
/// </summary>
public int? KeySize { get; set; }

/// <summary>
/// Gets or sets the public exponent for a RSA key.
/// </summary>
public int? PublicExponent { get; set; }

/// <summary>
/// Gets a value indicating whether to create a hardware-protected key in a hardware security module (HSM).
/// </summary>
Expand Down
16 changes: 16 additions & 0 deletions sdk/keyvault/Azure.Security.KeyVault.Keys/src/ImportKeyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ public class ImportKeyOptions : IJsonSerializable
private const string KeyPropertyName = "key";
private const string TagsPropertyName = "tags";
private const string HsmPropertyName = "hsm";
private const string ReleasePolicyPropertyName = "release_policy";

private static readonly JsonEncodedText s_keyPropertyNameBytes = JsonEncodedText.Encode(KeyPropertyName);
private static readonly JsonEncodedText s_tagsPropertyNameBytes = JsonEncodedText.Encode(TagsPropertyName);
private static readonly JsonEncodedText s_hsmPropertyNameBytes = JsonEncodedText.Encode(HsmPropertyName);
private static readonly JsonEncodedText s_releasePolicyPropertyNameBytes = JsonEncodedText.Encode(ReleasePolicyPropertyName);

/// <summary>
/// Initializes a new instance of the <see cref="ImportKeyOptions"/> class.
Expand Down Expand Up @@ -54,6 +56,11 @@ public ImportKeyOptions(string name, JsonWebKey keyMaterial)
/// </summary>
public bool? HardwareProtected { get; set; }

/// <summary>
/// Gets or sets the policy rules under which the key can be exported.
/// </summary>
public KeyReleasePolicy ReleasePolicy { get; set; }

/// <summary>
/// Gets additional properties of the <see cref="KeyVaultKey"/>.
/// </summary>
Expand Down Expand Up @@ -88,6 +95,15 @@ void IJsonSerializable.WriteProperties(Utf8JsonWriter json)
{
json.WriteBoolean(s_hsmPropertyNameBytes, HardwareProtected.Value);
}

if (ReleasePolicy != null)
{
json.WriteStartObject(s_releasePolicyPropertyNameBytes);

ReleasePolicy.WriteProperties(json);

json.WriteEndObject();
}
}
}
}
18 changes: 17 additions & 1 deletion sdk/keyvault/Azure.Security.KeyVault.Keys/src/KeyAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ internal struct KeyAttributes
private const string UpdatedPropertyName = "updated";
private const string RecoverableDaysPropertyName = "recoverableDays";
private const string RecoveryLevelPropertyName = "recoveryLevel";
private const string ExportablePropertyName = "exportable";

private static readonly JsonEncodedText s_enabledPropertyNameBytes = JsonEncodedText.Encode(EnabledPropertyName);
private static readonly JsonEncodedText s_notBeforePropertyNameBytes = JsonEncodedText.Encode(NotBeforePropertyName);
private static readonly JsonEncodedText s_expiresPropertyNameBytes = JsonEncodedText.Encode(ExpiresPropertyName);
private static readonly JsonEncodedText s_exportablePropertyNameBytes = JsonEncodedText.Encode(ExportablePropertyName);

public bool? Enabled { get; set; }

Expand All @@ -34,7 +36,13 @@ internal struct KeyAttributes

public string RecoveryLevel { get; internal set; }

internal bool ShouldSerialize => Enabled.HasValue && NotBefore.HasValue && ExpiresOn.HasValue;
public bool? Exportable { get; internal set; }

internal bool ShouldSerialize =>
Enabled.HasValue &&
NotBefore.HasValue &&
ExpiresOn.HasValue &&
Exportable.HasValue;

internal void ReadProperties(JsonElement json)
{
Expand Down Expand Up @@ -63,6 +71,9 @@ internal void ReadProperties(JsonElement json)
case RecoveryLevelPropertyName:
RecoveryLevel = prop.Value.GetString();
break;
case ExportablePropertyName:
Exportable = prop.Value.GetBoolean();
break;
}
}
}
Expand All @@ -84,6 +95,11 @@ internal void WriteProperties(Utf8JsonWriter json)
json.WriteNumber(s_expiresPropertyNameBytes, ExpiresOn.Value.ToUnixTimeSeconds());
}

if (Exportable.HasValue)
{
json.WriteBoolean(s_exportablePropertyNameBytes, Exportable.Value);
}

// Created is read-only don't serialize
// Updated is read-only don't serialize
// RecoverableDays is read-only don't serialize
Expand Down
108 changes: 108 additions & 0 deletions sdk/keyvault/Azure.Security.KeyVault.Keys/src/KeyClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Azure.Core.Pipeline;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -980,6 +981,7 @@ public virtual async Task<Response<KeyVaultKey>> RestoreKeyBackupAsync(byte[] ba
/// <param name="name">The name of the key.</param>
/// <param name="keyMaterial">The <see cref="JsonWebKey"/> being imported.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>The <see cref="KeyVaultKey"/> that was imported.</returns>
/// <exception cref="ArgumentException"><paramref name="name"/> is an empty string.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name"/> or <paramref name="keyMaterial"/> is null.</exception>
/// <exception cref="RequestFailedException">The server returned an error. See <see cref="Exception.Message"/> for details returned from the server.</exception>
Expand Down Expand Up @@ -1017,6 +1019,7 @@ public virtual Response<KeyVaultKey> ImportKey(string name, JsonWebKey keyMateri
/// <param name="name">The name of the key.</param>
/// <param name="keyMaterial">The <see cref="JsonWebKey"/> being imported.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>The <see cref="KeyVaultKey"/> that was imported.</returns>
/// <exception cref="ArgumentException"><paramref name="name"/> is an empty string.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name"/> or <paramref name="keyMaterial"/> is null.</exception>
/// <exception cref="RequestFailedException">The server returned an error. See <see cref="Exception.Message"/> for details returned from the server.</exception>
Expand Down Expand Up @@ -1108,5 +1111,110 @@ public virtual async Task<Response<KeyVaultKey>> ImportKeyAsync(ImportKeyOptions
throw;
}
}

/// <summary>
/// Exports the latest version of a <see cref="KeyVaultKey"/> including the private key if originally created with <see cref="CreateKeyOptions.Exportable"/> set to true,
/// or imported with <see cref="KeyProperties.Exportable"/> in <see cref="ImportKeyOptions"/> set to true.
/// </summary>
/// <remarks>
/// Requires the <see cref="KeyOperation.Export"/> permission.
/// </remarks>
/// <param name="name">The name of the key to export.</param>
/// <param name="environment">The target environment assertion.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>The <see cref="KeyVaultKey"/> that was exported along with the private key if exportable.</returns>
/// <exception cref="ArgumentException"><paramref name="name"/> or <paramref name="environment"/> is an empty string.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name"/> or <paramref name="environment"/> is null.</exception>
/// <seealso cref="ExportKey(string, string, string, CancellationToken)"/>
public virtual Response<KeyVaultKey> ExportKey(string name, string environment, CancellationToken cancellationToken = default) =>
ExportKey(name, null, environment, cancellationToken);


/// <summary>
/// Exports the latest version of a <see cref="KeyVaultKey"/> including the private key if originally created with <see cref="CreateKeyOptions.Exportable"/> set to true,
/// or imported with <see cref="KeyProperties.Exportable"/> in <see cref="ImportKeyOptions"/> set to true.
/// </summary>
/// <remarks>
/// Requires the <see cref="KeyOperation.Export"/> permission.
/// </remarks>
/// <param name="name">The name of the key to export.</param>
/// <param name="environment">The target environment assertion.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>The <see cref="KeyVaultKey"/> that was exported along with the private key if exportable.</returns>
/// <exception cref="ArgumentException"><paramref name="name"/> or <paramref name="environment"/> is an empty string.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name"/> or <paramref name="environment"/> is null.</exception>
/// <seealso cref="ExportKeyAsync(string, string, string, CancellationToken)"/>
public virtual async Task<Response<KeyVaultKey>> ExportKeyAsync(string name, string environment, CancellationToken cancellationToken = default) =>
await ExportKeyAsync(name, null, environment, cancellationToken).ConfigureAwait(false);

/// <summary>
/// Exports a specific version of a <see cref="KeyVaultKey"/> including the private key if originally created with <see cref="CreateKeyOptions.Exportable"/> set to true,
/// or imported with <see cref="KeyProperties.Exportable"/> in <see cref="ImportKeyOptions"/> set to true.
/// </summary>
/// <remarks>
/// Requires the <see cref="KeyOperation.Export"/> permission.
/// </remarks>
/// <param name="name">The name of the key to export.</param>
/// <param name="version">The optional version of the key to export.</param>
/// <param name="environment">The target environment assertion.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>The <see cref="KeyVaultKey"/> that was exported along with the private key if exportable.</returns>
/// <exception cref="ArgumentException"><paramref name="name"/> or <paramref name="environment"/> is an empty string.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name"/> or <paramref name="environment"/> is null.</exception>
/// <seealso cref="ExportKey(string, string, CancellationToken)"/>
public virtual Response<KeyVaultKey> ExportKey(string name, string version, string environment, CancellationToken cancellationToken = default)
{
Argument.AssertNotNullOrEmpty(name, nameof(name));
Argument.AssertNotNullOrEmpty(environment, nameof(environment));

using DiagnosticScope scope = _pipeline.CreateScope($"{nameof(KeyClient)}.{nameof(ExportKey)}");
scope.AddAttribute("key", name);
scope.Start();

try
{
return _pipeline.SendRequest(RequestMethod.Post, new KeyExportParameters(environment), () => new KeyVaultKey(name), cancellationToken, KeysPath, name, "/", version, "/export");
}
catch (Exception e)
{
scope.Failed(e);
throw;
}
}

/// <summary>
/// Exports a specific version of a <see cref="KeyVaultKey"/> including the private key if originally created with <see cref="CreateKeyOptions.Exportable"/> set to true,
/// or imported with <see cref="KeyProperties.Exportable"/> in <see cref="ImportKeyOptions"/> set to true.
/// </summary>
/// <remarks>
/// Requires the <see cref="KeyOperation.Export"/> permission.
/// </remarks>
/// <param name="name">The name of the key to export.</param>
/// <param name="version">The optional version of the key to export.</param>
/// <param name="environment">The target environment assertion.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns>The <see cref="KeyVaultKey"/> that was exported along with the private key if exportable.</returns>
/// <exception cref="ArgumentException"><paramref name="name"/> or <paramref name="environment"/> is an empty string.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name"/> or <paramref name="environment"/> is null.</exception>
/// <seealso cref="ExportKeyAsync(string, string, CancellationToken)"/>
public virtual async Task<Response<KeyVaultKey>> ExportKeyAsync(string name, string version, string environment, CancellationToken cancellationToken = default)
{
Argument.AssertNotNullOrEmpty(name, nameof(name));
Argument.AssertNotNullOrEmpty(environment, nameof(environment));

using DiagnosticScope scope = _pipeline.CreateScope($"{nameof(KeyClient)}.{nameof(ExportKey)}");
scope.AddAttribute("key", name);
scope.Start();

try
{
return await _pipeline.SendRequestAsync(RequestMethod.Post, new KeyExportParameters(environment), () => new KeyVaultKey(name), cancellationToken, KeysPath, name, "/", version, "/export").ConfigureAwait(false);
}
catch (Exception e)
{
scope.Failed(e);
throw;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Text.Json;

namespace Azure.Security.KeyVault.Keys
{
internal class KeyExportParameters : IJsonSerializable
{
private static readonly JsonEncodedText s_environmentPropertyNameBytes = JsonEncodedText.Encode("env");

internal KeyExportParameters(string environment)
{
Environment = environment;
}

public string Environment { get; }

public void WriteProperties(Utf8JsonWriter json)
{
json.WriteString(s_environmentPropertyNameBytes, Environment);
}
}
}
5 changes: 5 additions & 0 deletions sdk/keyvault/Azure.Security.KeyVault.Keys/src/KeyOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public KeyOperation(string value)
/// </summary>
public static KeyOperation Import { get; } = new KeyOperation("import");

/// <summary>
/// Gets a value that indicates the key can be exported using the <see cref="KeyClient.ExportKeyAsync(string, string, CancellationToken)"/> or <see cref="KeyClient.ExportKey(string, string, CancellationToken)"/> methods.
/// </summary>
public static KeyOperation Export { get; } = new KeyOperation("export");

/// <summary>
/// Determines if two <see cref="KeyOperation"/> values are the same.
/// </summary>
Expand Down
25 changes: 25 additions & 0 deletions sdk/keyvault/Azure.Security.KeyVault.Keys/src/KeyProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ public class KeyProperties : IJsonDeserializable
private const string ManagedPropertyName = "managed";
private const string AttributesPropertyName = "attributes";
private const string TagsPropertyName = "tags";
private const string ReleasePolicyPropertyName = "release_policy";

private static readonly JsonEncodedText s_attributesPropertyNameBytes = JsonEncodedText.Encode(AttributesPropertyName);
private static readonly JsonEncodedText s_releasePolicyPropertyNameBytes = JsonEncodedText.Encode(ReleasePolicyPropertyName);

internal Dictionary<string, string> _tags;

Expand Down Expand Up @@ -122,6 +124,16 @@ public KeyProperties(Uri id)
/// <value>Possible values include <c>Purgeable</c>, <c>Recoverable+Purgeable</c>, <c>Recoverable</c>, and <c>Recoverable+ProtectedSubscription</c>.</value>
public string RecoveryLevel { get => _attributes.RecoveryLevel; internal set => _attributes.RecoveryLevel = value; }

/// <summary>
/// Gets or sets a value indicating whether the private key can be exported.
/// </summary>
public bool? Exportable { get => _attributes.Exportable; set => _attributes.Exportable = value; }

/// <summary>
/// Gets or sets the policy rules under which the key can be exported.
/// </summary>
public KeyReleasePolicy ReleasePolicy { get; set; }

/// <summary>
/// Parses the key identifier into the <see cref="VaultUri"/>, <see cref="Name"/>, and <see cref="Version"/> of the key.
/// </summary>
Expand Down Expand Up @@ -162,6 +174,10 @@ internal void ReadProperty(JsonProperty prop)
Tags[tagProp.Name] = tagProp.Value.GetString();
}
break;
case ReleasePolicyPropertyName:
ReleasePolicy = new KeyReleasePolicy();
ReleasePolicy.ReadProperties(prop.Value);
break;
}
}

Expand All @@ -183,6 +199,15 @@ internal void WriteAttributes(Utf8JsonWriter json)

json.WriteEndObject();
}

if (ReleasePolicy != null)
{
json.WriteStartObject(s_releasePolicyPropertyNameBytes);

ReleasePolicy.WriteProperties(json);

json.WriteEndObject();
}
}

void IJsonDeserializable.ReadProperties(JsonElement json) => ReadProperties(json);
Expand Down
Loading

0 comments on commit 7865506

Please sign in to comment.