diff --git a/src/KeyVault/KeyVault.Test/ScenarioTests/OctHsmKeyTests.cs b/src/KeyVault/KeyVault.Test/ScenarioTests/OctHsmKeyTests.cs new file mode 100644 index 000000000000..c830f85f48c1 --- /dev/null +++ b/src/KeyVault/KeyVault.Test/ScenarioTests/OctHsmKeyTests.cs @@ -0,0 +1,50 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.WindowsAzure.Commands.ScenarioTest; +using Xunit; + +namespace Microsoft.Azure.Commands.KeyVault.Test.ScenarioTests +{ + /// + /// Scenario tests for oct (AES) HSM-backed keys on a Premium Azure Key Vault. + /// These exercise the end-to-end -KeyType oct -Destination HSM path which + /// the service stores as kty == 'oct-HSM'. + /// + /// Recorded with Azure Test Framework: run once in Record mode against a + /// real Premium vault, commit the generated SessionRecords/*.json files, + /// and CI replays them in Playback mode. See OctHsmKeyTests.ps1. + /// + public class OctHsmKeyTests : KeyVaultTestRunner + { + public OctHsmKeyTests(Xunit.Abstractions.ITestOutputHelper output) : base(output) + { + } + + [Fact] + [Trait(Category.AcceptanceType, Category.LiveOnly)] + public void TestCreateOctHsmKey() + { + TestRunner.RunTestScript("Test-CreateOctHsmKey"); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.LiveOnly)] + public void TestCreateOctHsmKeyAllSizes() + { + TestRunner.RunTestScript("Test-CreateOctHsmKeyAllSizes"); + } + + } + } diff --git a/src/KeyVault/KeyVault.Test/ScenarioTests/OctHsmKeyTests.ps1 b/src/KeyVault/KeyVault.Test/ScenarioTests/OctHsmKeyTests.ps1 new file mode 100644 index 000000000000..e72a941d0c81 --- /dev/null +++ b/src/KeyVault/KeyVault.Test/ScenarioTests/OctHsmKeyTests.ps1 @@ -0,0 +1,64 @@ +<# +.SYNOPSIS +Scenario tests for oct-HSM (AES, HSM-backed) keys on a Premium Azure Key Vault. + +oct-HSM keys require: + - Vault SKU = 'Premium' + - -KeyType oct + - -Destination HSM + - -Size in { 128, 192, 256 } + +The service rewrites the key type to 'oct-HSM' on the wire. + +Note: vaults are created with -DisableRbacAuthorization because the test +identity is a guest in the test tenant, so MS Graph UPN lookups fail. With +access-policy mode, New-AzKeyVault auto-adds a full-permission policy for the +caller, which is what these tests rely on for the data-plane calls. +#> +function Test-CreateOctHsmKey { + $resourceGroupLocation = Get-Location "Microsoft.Resources" "resourceGroups" "East US 2 EUAP" + $vaultLocation = Get-Location "Microsoft.KeyVault" "vaults" "East US 2 EUAP" + $resourceGroupName = (GetAssetName) + $vaultName = (GetAssetName) + $keyName = (GetAssetName) + + try { + New-AzResourceGroup -Name $resourceGroupName -Location $resourceGroupLocation + $vault = New-AzKeyVault -VaultName $vaultName -ResourceGroupName $resourceGroupName -Location $vaultLocation -Sku "Premium" -DisableRbacAuthorization + + # Create an oct-HSM key with the default 256-bit size + $key = $vault | Add-AzKeyVaultKey -Name $keyName -KeyType oct -Destination HSM -Size 256 + Assert-NotNull $key "Add-AzKeyVaultKey returned null" + Assert-AreEqual "oct-HSM" $key.Key.Kty "key type != 'oct-HSM'" + Assert-AreEqual $keyName $key.Name "key name mismatch" + + # Get-AzKeyVaultKey must round-trip the same kty + $got = Get-AzKeyVaultKey -VaultName $vaultName -Name $keyName + Assert-NotNull $got "Get-AzKeyVaultKey returned null" + Assert-AreEqual "oct-HSM" $got.Key.Kty "round-tripped key type != 'oct-HSM'" + } + finally { + Remove-AzResourceGroup -Name $resourceGroupName -Force + } +} + +function Test-CreateOctHsmKeyAllSizes { + $resourceGroupLocation = Get-Location "Microsoft.Resources" "resourceGroups" "East US 2 EUAP" + $vaultLocation = Get-Location "Microsoft.KeyVault" "vaults" "East US 2 EUAP" + $resourceGroupName = (GetAssetName) + $vaultName = (GetAssetName) + + try { + New-AzResourceGroup -Name $resourceGroupName -Location $resourceGroupLocation + $vault = New-AzKeyVault -VaultName $vaultName -ResourceGroupName $resourceGroupName -Location $vaultLocation -Sku "Premium" -DisableRbacAuthorization + + foreach ($size in 128, 192, 256) { + $keyName = (GetAssetName) + $key = $vault | Add-AzKeyVaultKey -Name $keyName -KeyType oct -Destination HSM -Size $size + Assert-AreEqual "oct-HSM" $key.Key.Kty "size=${size}: key type != 'oct-HSM'" + } + } + finally { + Remove-AzResourceGroup -Name $resourceGroupName -Force + } +} diff --git a/src/KeyVault/KeyVault.Test/UnitTests/AddKeyVaultOctKeyTests.cs b/src/KeyVault/KeyVault.Test/UnitTests/AddKeyVaultOctKeyTests.cs new file mode 100644 index 000000000000..f66be83bb308 --- /dev/null +++ b/src/KeyVault/KeyVault.Test/UnitTests/AddKeyVaultOctKeyTests.cs @@ -0,0 +1,251 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.KeyVault.Models; +using Microsoft.Azure.KeyVault.WebKey; +using Microsoft.WindowsAzure.Commands.ScenarioTest; +using Moq; +using Xunit; +using WebKey = Microsoft.Azure.KeyVault.WebKey; + +namespace Microsoft.Azure.Commands.KeyVault.Test.UnitTests +{ + /// + /// Verifies the cmdlet-level wiring for creating oct (AES) HSM-backed keys + /// via Add-AzKeyVaultKey, on both Premium AKV and Managed HSM. + /// + /// These are pure cmdlet plumbing tests: they mock IKeyVaultDataServiceClient + /// (the same type used by Track2DataClient) and assert that the cmdlet: + /// 1. Translates -KeyType oct -Destination HSM into PSKeyVaultKeyAttributes + /// with KeyType == "oct-HSM" before calling the service client. + /// 2. Forwards the requested -Size to the service client unchanged. + /// 3. Routes vault calls through CreateKey and HSM calls through + /// CreateManagedHsmKey. + /// + /// Server-side behavior (e.g. the Track2 SDK CreateOctKeyOptions wire format, + /// or the eventual KeyProperties.KeySize read-back) is intentionally out of + /// scope here and is covered by manual validation / Pester scripts. + /// + public class AddKeyVaultOctKeyTests : KeyVaultUnitTestBase + { + private const string HsmName = "hsmname"; + + private readonly AddAzureKeyVaultKey cmdlet; + private readonly Mock track2DataClientMock; + + public AddKeyVaultOctKeyTests() + { + base.SetupTest(); + + // KeyVaultUnitTestBase sets up keyVaultClientMock for Track1; create + // a separate mock for the Track2 client, since AddAzureKeyVaultKey + // routes oct/RSA/EC creation through Track2DataClient. + track2DataClientMock = new Mock(); + + cmdlet = new AddAzureKeyVaultKey + { + CommandRuntime = commandRuntimeMock.Object, + DataServiceClient = keyVaultClientMock.Object, + Track2DataClient = track2DataClientMock.Object, + }; + + // ConfirmAction / ShouldProcess must return true for ExecuteCmdlet + // to reach the create call. + commandRuntimeMock + .Setup(cr => cr.ShouldProcess(It.IsAny(), It.IsAny())) + .Returns(true); + } + + private static PSKeyVaultKey StubKey(string vaultName, string name, string kty) + { + return new PSKeyVaultKey + { + VaultName = vaultName, + Name = name, + Key = new WebKey.JsonWebKey { Kty = kty }, + Attributes = new PSKeyVaultKeyAttributes(true, null, null, kty, null, null), + }; + } + + [Theory] + [Trait(Category.AcceptanceType, Category.CheckIn)] + [InlineData(128)] + [InlineData(192)] + [InlineData(256)] + public void OctKeyOnVaultIsCreatedAsOctHsm(int size) + { + // Capture the attributes the cmdlet hands to the service client so + // we can assert KeyType is rewritten from 'oct' -> 'oct-HSM'. + PSKeyVaultKeyAttributes captured = null; + int? capturedSize = null; + + track2DataClientMock + .Setup(c => c.CreateKey(VaultName, KeyName, + It.IsAny(), It.IsAny(), It.IsAny())) + .Callback( + (_, __, attrs, sz, ___) => { captured = attrs; capturedSize = sz; }) + .Returns(StubKey(VaultName, KeyName, "oct-HSM")) + .Verifiable(); + + cmdlet.VaultName = VaultName; + cmdlet.Name = KeyName; + cmdlet.KeyType = JsonWebKeyType.Octet; // "oct" + cmdlet.Destination = "HSM"; + cmdlet.Size = size; + + cmdlet.ExecuteCmdlet(); + + track2DataClientMock.VerifyAll(); + Assert.NotNull(captured); + Assert.Equal("oct-HSM", captured.KeyType); + Assert.Equal(size, capturedSize); + + // Track1 client must not be used for creation on the vault path. + keyVaultClientMock.Verify(c => c.CreateKey( + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Theory] + [Trait(Category.AcceptanceType, Category.CheckIn)] + [InlineData(128)] + [InlineData(256)] + public void OctKeyOnManagedHsmRoutesToCreateManagedHsmKeyWithSize(int size) + { + PSKeyVaultKeyAttributes captured = null; + int? capturedSize = null; + + track2DataClientMock + .Setup(c => c.CreateManagedHsmKey(HsmName, KeyName, + It.IsAny(), It.IsAny(), It.IsAny())) + .Callback( + (_, __, attrs, sz, ___) => { captured = attrs; capturedSize = sz; }) + .Returns(StubKey(HsmName, KeyName, "oct-HSM")) + .Verifiable(); + + cmdlet.HsmName = HsmName; + cmdlet.Name = KeyName; + cmdlet.KeyType = JsonWebKeyType.Octet; // "oct" + // Note: -Destination is not used on the HSM path; Managed HSM keys + // are always HSM-backed, so the cmdlet does not rewrite the kty. + // The service still emits 'oct-HSM' on the wire. + cmdlet.Size = size; + + cmdlet.ExecuteCmdlet(); + + track2DataClientMock.VerifyAll(); + Assert.NotNull(captured); + // On the MHSM path the cmdlet does not rewrite kty: it forwards the + // user-supplied "oct" unchanged. Track2HsmClient is what unconditionally + // sets hardwareProtected=true when calling the SDK. + Assert.Equal(JsonWebKeyType.Octet, captured.KeyType); + Assert.Equal(size, capturedSize); + + // Vault create must not be invoked on the HSM path. + track2DataClientMock.Verify(c => c.CreateKey( + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void RsaKeyOnVaultWithHsmDestinationStaysAsRsaHsm() + { + // Regression guard: the new oct-HSM rewrite must not disturb the + // existing RSA / EC -> RSA-HSM / EC-HSM rewrites. + PSKeyVaultKeyAttributes captured = null; + + track2DataClientMock + .Setup(c => c.CreateKey(VaultName, KeyName, + It.IsAny(), It.IsAny(), It.IsAny())) + .Callback( + (_, __, attrs, ___, ____) => captured = attrs) + .Returns(StubKey(VaultName, KeyName, JsonWebKeyType.RsaHsm)); + + cmdlet.VaultName = VaultName; + cmdlet.Name = KeyName; + cmdlet.KeyType = JsonWebKeyType.Rsa; + cmdlet.Destination = "HSM"; + cmdlet.Size = 2048; + + cmdlet.ExecuteCmdlet(); + + Assert.NotNull(captured); + Assert.Equal(JsonWebKeyType.RsaHsm, captured.KeyType); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void RsaKeyOnVaultWithSoftwareDestinationStaysAsRsa() + { + // Software-backed RSA on a Standard or Premium AKV is the default + // path. The cmdlet must NOT rewrite the kty (stays plain "RSA") + // and must still route through Track2DataClient.CreateKey. + PSKeyVaultKeyAttributes captured = null; + + track2DataClientMock + .Setup(c => c.CreateKey(VaultName, KeyName, + It.IsAny(), It.IsAny(), It.IsAny())) + .Callback( + (_, __, attrs, ___, ____) => captured = attrs) + .Returns(StubKey(VaultName, KeyName, JsonWebKeyType.Rsa)) + .Verifiable(); + + cmdlet.VaultName = VaultName; + cmdlet.Name = KeyName; + cmdlet.KeyType = JsonWebKeyType.Rsa; + cmdlet.Destination = "Software"; + cmdlet.Size = 2048; + + cmdlet.ExecuteCmdlet(); + + track2DataClientMock.VerifyAll(); + Assert.NotNull(captured); + Assert.Equal(JsonWebKeyType.Rsa, captured.KeyType); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void OctKeyOnVaultWithoutHsmDestinationIsNotRewritten() + { + // Documents that the cmdlet only rewrites oct -> oct-HSM when + // -Destination HSM is set on a vault. Without it, kty stays "oct" + // and the request would be rejected server-side (no software-backed + // oct keys exist on AKV). We can't exercise the service rejection + // here, but we can pin the cmdlet-side contract. + PSKeyVaultKeyAttributes captured = null; + + track2DataClientMock + .Setup(c => c.CreateKey(VaultName, KeyName, + It.IsAny(), It.IsAny(), It.IsAny())) + .Callback( + (_, __, attrs, ___, ____) => captured = attrs) + .Returns(StubKey(VaultName, KeyName, JsonWebKeyType.Octet)); + + cmdlet.VaultName = VaultName; + cmdlet.Name = KeyName; + cmdlet.KeyType = JsonWebKeyType.Octet; // "oct" + // No Destination set -> kty must NOT be rewritten to "oct-HSM". + cmdlet.Size = 256; + + cmdlet.ExecuteCmdlet(); + + Assert.NotNull(captured); + Assert.Equal(JsonWebKeyType.Octet, captured.KeyType); + Assert.NotEqual("oct-HSM", captured.KeyType); + } + } +} diff --git a/src/KeyVault/KeyVault.Test/UnitTests/Track2KeyOptionsFactoryTests.cs b/src/KeyVault/KeyVault.Test/UnitTests/Track2KeyOptionsFactoryTests.cs new file mode 100644 index 000000000000..6339828ca437 --- /dev/null +++ b/src/KeyVault/KeyVault.Test/UnitTests/Track2KeyOptionsFactoryTests.cs @@ -0,0 +1,211 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Linq; + +using Azure.Security.KeyVault.Keys; + +using Microsoft.Azure.Commands.KeyVault.Models; +using Microsoft.Azure.Commands.KeyVault.Track2Models; +using Microsoft.WindowsAzure.Commands.ScenarioTest; + +using Xunit; + +namespace Microsoft.Azure.Commands.KeyVault.Test.UnitTests +{ + /// + /// Pure unit tests for . + /// + /// These tests pin the translation from + /// (+ size + curve) into the SDK's Create*KeyOptions objects. They are + /// the safety net that lets us refactor the per-type dispatch in + /// and without + /// regressing the wire-level shape of the create requests. + /// + /// They deliberately do NOT touch the SDK's KeyClient — option-building + /// is pure, so no mock transport is needed. + /// + public class Track2KeyOptionsFactoryTests + { + private const string KeyName = "test-key"; + + private static PSKeyVaultKeyAttributes EmptyAttrs() => new PSKeyVaultKeyAttributes(); + + // ---------- BuildRsa ---------- + + [Theory] + [Trait(Category.AcceptanceType, Category.CheckIn)] + [InlineData(2048, true)] + [InlineData(3072, true)] + [InlineData(4096, true)] + [InlineData(2048, false)] + public void BuildRsa_SetsKeySizeAndHsmFlag(int size, bool hardwareProtected) + { + var options = Track2KeyOptionsFactory.BuildRsaKeyOptions(KeyName, hardwareProtected, EmptyAttrs(), size); + + Assert.Equal(KeyName, options.Name); + Assert.Equal(size, options.KeySize); + Assert.Equal(hardwareProtected, options.HardwareProtected); + } + + // ---------- BuildEc ---------- + + [Theory] + [Trait(Category.AcceptanceType, Category.CheckIn)] + [InlineData("P-256")] + [InlineData("P-384")] + [InlineData("P-521")] + [InlineData("P-256K")] + public void BuildEc_WithCurveName_SetsCurve(string curveName) + { + var options = Track2KeyOptionsFactory.BuildEcKeyOptions(KeyName, hardwareProtected: true, EmptyAttrs(), curveName); + + Assert.Equal(KeyName, options.Name); + Assert.True(options.HardwareProtected); + Assert.NotNull(options.CurveName); + Assert.Equal(curveName, options.CurveName.Value.ToString()); + } + + [Theory] + [Trait(Category.AcceptanceType, Category.CheckIn)] + [InlineData(null)] + [InlineData("")] + public void BuildEc_NullOrEmptyCurveName_LeavesCurveNull(string curveName) + { + var options = Track2KeyOptionsFactory.BuildEcKeyOptions(KeyName, hardwareProtected: false, EmptyAttrs(), curveName); + + // A null CurveName lets the service apply its default; this is the + // documented behavior the original code preserved explicitly. + Assert.Null(options.CurveName); + Assert.False(options.HardwareProtected); + } + + // ---------- BuildOct ---------- + + [Theory] + [Trait(Category.AcceptanceType, Category.CheckIn)] + [InlineData(128, true)] + [InlineData(192, true)] + [InlineData(256, true)] + [InlineData(256, false)] + public void BuildOct_SetsKeySizeAndHardwareProtectedFromCaller(int size, bool hardwareProtected) + { + // The factory no longer hardcodes hardwareProtected: callers (Track2VaultClient / + // Track2HsmClient) decide based on the requested KeyType (Oct vs OctHsm) and the + // backing service. The factory just forwards the flag verbatim. + var options = Track2KeyOptionsFactory.BuildOctKeyOptions(KeyName, hardwareProtected, EmptyAttrs(), size); + + Assert.Equal(KeyName, options.Name); + Assert.Equal(size, options.KeySize); + Assert.Equal(hardwareProtected, options.HardwareProtected); + } + + // ---------- ApplyCommonAttributes ---------- + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void ApplyCommonAttributes_CopiesScalarFields() + { + var notBefore = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var expires = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + var attrs = new PSKeyVaultKeyAttributes + { + Enabled = true, + NotBefore = notBefore, + Expires = expires, + Exportable = false, + }; + + // Build via BuildRsa just to exercise ApplyCommonAttributes through + // the public surface; the asserts only inspect common fields. + var options = Track2KeyOptionsFactory.BuildRsaKeyOptions(KeyName, hardwareProtected: false, attrs, 2048); + + Assert.True(options.Enabled); + Assert.Equal(notBefore, options.NotBefore); + Assert.Equal(expires, options.ExpiresOn); + Assert.Equal(false, options.Exportable); + Assert.Null(options.ReleasePolicy); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void ApplyCommonAttributes_NullKeyOps_DoesNotThrow() + { + var attrs = new PSKeyVaultKeyAttributes { KeyOps = null }; + + var options = Track2KeyOptionsFactory.BuildRsaKeyOptions(KeyName, hardwareProtected: false, attrs, 2048); + + Assert.Empty(options.KeyOperations); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void ApplyCommonAttributes_KeyOps_AddsAllAsKeyOperation() + { + var attrs = new PSKeyVaultKeyAttributes { KeyOps = new[] { "encrypt", "decrypt", "wrapKey" } }; + + var options = Track2KeyOptionsFactory.BuildRsaKeyOptions(KeyName, hardwareProtected: false, attrs, 2048); + + // KeyOperation values round-trip through string equality with the + // input op names; that's what the JWK kty key_ops field expects. + var ops = options.KeyOperations.Select(op => op.ToString()).ToArray(); + Assert.Equal(new[] { "encrypt", "decrypt", "wrapKey" }, ops); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void ApplyCommonAttributes_NullTags_DoesNotThrow() + { + var attrs = new PSKeyVaultKeyAttributes { Tags = null }; + + var options = Track2KeyOptionsFactory.BuildOctKeyOptions(KeyName, hardwareProtected: true, attrs, 256); + + Assert.Empty(options.Tags); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void ApplyCommonAttributes_Tags_CopiedAsKeyValuePairs() + { + var attrs = new PSKeyVaultKeyAttributes + { + Tags = new Hashtable { { "scenario", "aes-validate" }, { "runId", "abc123" } } + }; + + var options = Track2KeyOptionsFactory.BuildOctKeyOptions(KeyName, hardwareProtected: true, attrs, 256); + + Assert.Equal(2, options.Tags.Count); + Assert.Equal("aes-validate", options.Tags["scenario"]); + Assert.Equal("abc123", options.Tags["runId"]); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public void ApplyCommonAttributes_NullReleasePolicy_LeavesPropertyNull() + { + // Guards the `attrs.ReleasePolicy?.ToKeyReleasePolicy()` null-conditional: + // a missing release policy must not produce an empty KeyReleasePolicy. + var attrs = new PSKeyVaultKeyAttributes { /* ReleasePolicy left null */ }; + + var options = Track2KeyOptionsFactory.BuildEcKeyOptions(KeyName, hardwareProtected: false, attrs, "P-256"); + + Assert.Null(options.ReleasePolicy); + } + } +} + + diff --git a/src/KeyVault/KeyVault/ChangeLog.md b/src/KeyVault/KeyVault/ChangeLog.md index d1832fa82c1d..93cd541330b9 100644 --- a/src/KeyVault/KeyVault/ChangeLog.md +++ b/src/KeyVault/KeyVault/ChangeLog.md @@ -18,8 +18,9 @@ - Additional information about change #1 --> ## Upcoming Release +* Added support for creating AES (Advanced Encryption Standard, `oct`) HSM (hardware security module) keys in Azure Key Vault Premium via `Add-AzKeyVaultKey -KeyType oct -Destination HSM` (resulting `KeyType` is `oct-HSM`). * Fixed `New-AzKeyVault` `RequestDisallowedByPolicy` error -* Removed certificate-backed items from `Get-AzKeyVaultKey`/`Get-AzKeyVaultSecret`; use `Get-AzKeyVaultCertificate` instead [#26217] +* [Breaking Change] Removed certificate-backed items from `Get-AzKeyVaultKey`/`Get-AzKeyVaultSecret`; use `Get-AzKeyVaultCertificate` instead [#26217] ## Version 6.4.3 * Added upcoming breaking change warning messages to `Get-AzKeyVaultKey` and `Get-AzKeyVaultSecret` for filtering certificate-backed keys and secrets. diff --git a/src/KeyVault/KeyVault/Commands/Key/AddAzureKeyVaultKey.cs b/src/KeyVault/KeyVault/Commands/Key/AddAzureKeyVaultKey.cs index 4bfb45364ccf..0b189f76222c 100644 --- a/src/KeyVault/KeyVault/Commands/Key/AddAzureKeyVaultKey.cs +++ b/src/KeyVault/KeyVault/Commands/Key/AddAzureKeyVaultKey.cs @@ -560,7 +560,7 @@ internal PSKeyVaultKeyAttributes CreateKeyAttributes() { if (KeyType == JsonWebKeyType.Rsa) KeyType = JsonWebKeyType.RsaHsm; else if (KeyType == JsonWebKeyType.EllipticCurve) KeyType = JsonWebKeyType.EllipticCurveHsm; - // oct (AES) is only supported by managed HSM + else if (KeyType == JsonWebKeyType.Octet) KeyType = "oct-HSM"; } } diff --git a/src/KeyVault/KeyVault/Commands/Key/InvokeAzureKeyVaultKeyOperation.cs b/src/KeyVault/KeyVault/Commands/Key/InvokeAzureKeyVaultKeyOperation.cs index 4797c56045c0..fcb28a7f6792 100644 --- a/src/KeyVault/KeyVault/Commands/Key/InvokeAzureKeyVaultKeyOperation.cs +++ b/src/KeyVault/KeyVault/Commands/Key/InvokeAzureKeyVaultKeyOperation.cs @@ -1,14 +1,8 @@ -using Microsoft.Azure.Commands.Common.Exceptions; -using Microsoft.Azure.Commands.KeyVault.Models; +using Microsoft.Azure.Commands.KeyVault.Models; using Microsoft.Azure.Commands.ResourceManager.Common.ArgumentCompleters; -using Microsoft.WindowsAzure.Commands.Common.CustomAttributes; -using Microsoft.WindowsAzure.Commands.Utilities.Common; using System; -using System.Collections; using System.Management.Automation; -using System.Security; -using System.Text; namespace Microsoft.Azure.Commands.KeyVault.Commands.Key { @@ -52,7 +46,16 @@ enum Operations [Parameter(Mandatory = true, HelpMessage = "Algorithm identifier")] [ValidateNotNullOrEmpty] - [PSArgumentCompleter("RSA-OAEP", "RSA-OAEP-256", "RSA1_5")] + [PSArgumentCompleter( + // RSA (Vault + HSM) — Encrypt/Decrypt/Wrap/Unwrap + "RSA1_5", "RSA-OAEP", "RSA-OAEP-256", + // AES Encrypt/Decrypt (HSM) + "A128CBC", "A128CBCPAD", "A128GCM", + "A192CBC", "A192CBCPAD", "A192GCM", + "A256CBC", "A256CBCPAD", "A256GCM", + // AES Key Wrap/Unwrap (HSM) + "A128KW", "A192KW", "A256KW", + "CKM_AES_KEY_WRAP", "CKM_AES_KEY_WRAP_PAD")] [Alias("EncryptionAlgorithm", "WrapAlgorithm")] public string Algorithm { get; set; } diff --git a/src/KeyVault/KeyVault/Track2Models/Track2HsmClient.cs b/src/KeyVault/KeyVault/Track2Models/Track2HsmClient.cs index 79e2ce41d537..3b636269f9d6 100644 --- a/src/KeyVault/KeyVault/Track2Models/Track2HsmClient.cs +++ b/src/KeyVault/KeyVault/Track2Models/Track2HsmClient.cs @@ -14,16 +14,15 @@ using KeyVaultProperties = Microsoft.Azure.Commands.KeyVault.Properties; using Azure.Security.KeyVault.Keys.Cryptography; using Microsoft.WindowsAzure.Commands.Utilities.Common; -using System.Xml; -using Microsoft.Azure.Management.WebSites.Version2016_09_01.Models; + using Microsoft.Azure.Commands.Common.Exceptions; namespace Microsoft.Azure.Commands.KeyVault.Track2Models { internal class Track2HsmClient { - private Track2TokenCredential _credential; - private VaultUriHelper _uriHelper; + private readonly Track2TokenCredential _credential; + private readonly VaultUriHelper _uriHelper; private KeyClient CreateKeyClient(string hsmName) => new KeyClient(_uriHelper.CreateVaultUri(hsmName), _credential); private KeyVaultBackupClient CreateBackupClient(string hsmName) => new KeyVaultBackupClient(_uriHelper.CreateVaultUri(hsmName), _credential); @@ -83,70 +82,25 @@ internal PSKeyVaultKey CreateKey(string managedHsmName, string keyName, PSKeyVau private PSKeyVaultKey CreateKey(KeyClient client, string keyName, PSKeyVaultKeyAttributes keyAttributes, int? size, string curveName) { - // todo duplicated code with Track2VaultClient.CreateKey - CreateKeyOptions options; - bool isHsm = keyAttributes.KeyType == KeyType.RsaHsm || keyAttributes.KeyType == KeyType.EcHsm; - if (keyAttributes.KeyType == KeyType.Rsa || keyAttributes.KeyType == KeyType.RsaHsm) { - options = new CreateRsaKeyOptions(keyName, isHsm) { KeySize = size }; + var options = Track2KeyOptionsFactory.BuildRsaKeyOptions(keyName, hardwareProtected: true, keyAttributes, size); + return new PSKeyVaultKey(client.CreateRsaKey(options).Value, _uriHelper, isHsm: true); } - else if (keyAttributes.KeyType == KeyType.Ec || keyAttributes.KeyType == KeyType.EcHsm) + if (keyAttributes.KeyType == KeyType.Ec || keyAttributes.KeyType == KeyType.EcHsm) { - options = new CreateEcKeyOptions(keyName, isHsm); - if (string.IsNullOrEmpty(curveName)) - { - (options as CreateEcKeyOptions).CurveName = null; - } - else - { - (options as CreateEcKeyOptions).CurveName = new KeyCurveName(curveName); - } - } - else - { - options = new CreateKeyOptions(); - } - - // Common key attributes - options.NotBefore = keyAttributes.NotBefore; - options.ExpiresOn = keyAttributes.Expires; - options.Enabled = keyAttributes.Enabled; - options.Exportable = keyAttributes.Exportable; - options.ReleasePolicy = keyAttributes.ReleasePolicy?.ToKeyReleasePolicy(); - - if (keyAttributes.KeyOps != null) - { - foreach (var keyOp in keyAttributes.KeyOps) - { - options.KeyOperations.Add(new KeyOperation(keyOp)); - } + var options = Track2KeyOptionsFactory.BuildEcKeyOptions(keyName, hardwareProtected: true, keyAttributes, curveName); + return new PSKeyVaultKey(client.CreateEcKey(options).Value, _uriHelper, isHsm: true); } - - if (keyAttributes.Tags != null) + if (keyAttributes.KeyType == KeyType.Oct || keyAttributes.KeyType == KeyType.OctHsm) { - foreach (DictionaryEntry entry in keyAttributes.Tags) - { - options.Tags.Add(entry.Key.ToString(), entry.Value.ToString()); - } + // Managed HSM is HSM-backed by definition; oct keys are always + // hardware-protected here regardless of the requested KeyType. + var options = Track2KeyOptionsFactory.BuildOctKeyOptions(keyName, hardwareProtected: true, keyAttributes, size); + return new PSKeyVaultKey(client.CreateOctKey(options).Value, _uriHelper, isHsm: true); } - if (keyAttributes.KeyType == KeyType.Rsa || keyAttributes.KeyType == KeyType.RsaHsm) - { - return new PSKeyVaultKey(client.CreateRsaKey(options as CreateRsaKeyOptions).Value, _uriHelper, isHsm: true); - } - else if (keyAttributes.KeyType == KeyType.Ec || keyAttributes.KeyType == KeyType.EcHsm) - { - return new PSKeyVaultKey(client.CreateEcKey(options as CreateEcKeyOptions).Value, _uriHelper, isHsm: true); - } - else if (keyAttributes.KeyType == KeyType.Oct || keyAttributes.KeyType.ToString() == "oct-HSM") - { - return new PSKeyVaultKey(client.CreateKey(keyName, KeyType.Oct, options).Value, _uriHelper, isHsm: true); - } - else - { - throw new NotSupportedException($"{keyAttributes.KeyType} is not supported"); - } + throw new NotSupportedException($"{keyAttributes.KeyType} is not supported"); } internal PSKeyOperationResult Decrypt(string managedHsmName, string keyName, string version, byte[] value, string encryptAlgorithm) diff --git a/src/KeyVault/KeyVault/Track2Models/Track2KeyOptionsFactory.cs b/src/KeyVault/KeyVault/Track2Models/Track2KeyOptionsFactory.cs new file mode 100644 index 000000000000..8b70681427a8 --- /dev/null +++ b/src/KeyVault/KeyVault/Track2Models/Track2KeyOptionsFactory.cs @@ -0,0 +1,119 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Collections; + +using Azure.Security.KeyVault.Keys; + +using Microsoft.Azure.Commands.KeyVault.Models; + +namespace Microsoft.Azure.Commands.KeyVault.Track2Models +{ + /// + /// Pure builders for the Track2 SDK Create*KeyOptions types. + /// + /// Both and + /// translate a + size + curve into a + /// concrete CreateRsaKeyOptions / CreateEcKeyOptions / + /// CreateOctKeyOptions and call the corresponding + /// KeyClient.Create*Key overload. Centralizing the option building + /// here: + /// - removes the duplication the original code carried as a // todo, + /// - keeps the option-building free of any SDK I/O so it can be unit + /// tested without an Azure endpoint or a mocked KeyClient, + /// - lets each client pass its own hardwareProtected flag (vault + /// derives it from the key type; MHSM always passes true). + /// + /// The actual SDK call and the PSKeyVaultKey wrapping (with the + /// per-client output isHsm flag) stay in each client's dispatcher. + /// + internal static class Track2KeyOptionsFactory + { + internal static CreateRsaKeyOptions BuildRsaKeyOptions(string keyName, bool hardwareProtected, + PSKeyVaultKeyAttributes attrs, int? size) + { + var options = new CreateRsaKeyOptions(keyName, hardwareProtected) { KeySize = size }; + ApplyCommonAttributes(options, attrs); + return options; + } + + internal static CreateEcKeyOptions BuildEcKeyOptions(string keyName, bool hardwareProtected, + PSKeyVaultKeyAttributes attrs, string curveName) + { + var options = new CreateEcKeyOptions(keyName, hardwareProtected) + { + // Empty/null curve leaves CurveName null so the service applies + // its default. Original code preserved this explicitly. + CurveName = string.IsNullOrEmpty(curveName) ? (KeyCurveName?)null : new KeyCurveName(curveName) + }; + ApplyCommonAttributes(options, attrs); + return options; + } + + /// + /// Builds for an octet (AES) key. + /// + /// The caller is responsible for passing the correct + /// flag: + /// + /// always passes true + /// (Managed HSM is HSM-backed by definition). + /// derives it from the + /// requested KeyType (OctHsm = true, Oct = false) so + /// that the user's -Destination choice is honored and any + /// service-side restriction (e.g. software oct not being supported on AKV) + /// surfaces as the authoritative service error rather than a silently + /// promoted HSM create. + /// + /// + internal static CreateOctKeyOptions BuildOctKeyOptions(string keyName, bool hardwareProtected, + PSKeyVaultKeyAttributes attrs, int? size) + { + var options = new CreateOctKeyOptions(keyName, hardwareProtected) { KeySize = size }; + ApplyCommonAttributes(options, attrs); + return options; + } + + /// + /// Copies fields common to every Create*KeyOptions: lifecycle + /// dates, enabled / exportable flags, release policy, key operations, + /// tags. Defensive against null + /// and null . + /// + internal static void ApplyCommonAttributes(CreateKeyOptions options, PSKeyVaultKeyAttributes attrs) + { + options.NotBefore = attrs.NotBefore; + options.ExpiresOn = attrs.Expires; + options.Enabled = attrs.Enabled; + options.Exportable = attrs.Exportable; + options.ReleasePolicy = attrs.ReleasePolicy?.ToKeyReleasePolicy(); + + if (attrs.KeyOps != null) + { + foreach (var keyOp in attrs.KeyOps) + { + options.KeyOperations.Add(new KeyOperation(keyOp)); + } + } + + if (attrs.Tags != null) + { + foreach (DictionaryEntry entry in attrs.Tags) + { + options.Tags.Add(entry.Key.ToString(), entry.Value.ToString()); + } + } + } + } +} diff --git a/src/KeyVault/KeyVault/Track2Models/Track2VaultClient.cs b/src/KeyVault/KeyVault/Track2Models/Track2VaultClient.cs index c88a72fc463f..d12eecfb7e97 100644 --- a/src/KeyVault/KeyVault/Track2Models/Track2VaultClient.cs +++ b/src/KeyVault/KeyVault/Track2Models/Track2VaultClient.cs @@ -7,7 +7,6 @@ using Microsoft.WindowsAzure.Commands.Utilities.Common; using System; -using System.Collections; using System.Collections.Generic; using System.Security; @@ -15,8 +14,8 @@ namespace Microsoft.Azure.Commands.KeyVault.Track2Models { internal class Track2VaultClient { - private Track2TokenCredential _credential; - private VaultUriHelper _vaultUriHelper; + private readonly Track2TokenCredential _credential; + private readonly VaultUriHelper _vaultUriHelper; // After a track 2 client is created, the vault / hsm uri associated to it cannot be changed // however azure powershell may deal with multiple vaults / hsms @@ -42,55 +41,31 @@ internal PSKeyVaultKey CreateKey(string vaultName, string keyName, PSKeyVaultKey private PSKeyVaultKey CreateKey(KeyClient client, string keyName, PSKeyVaultKeyAttributes keyAttributes, int? size, string curveName) { - CreateKeyOptions options; - bool isHsm = keyAttributes.KeyType == KeyType.RsaHsm || keyAttributes.KeyType == KeyType.EcHsm; + bool isHsm = keyAttributes.KeyType == KeyType.RsaHsm || keyAttributes.KeyType == KeyType.EcHsm || keyAttributes.KeyType == KeyType.OctHsm; if (keyAttributes.KeyType == KeyType.Rsa || keyAttributes.KeyType == KeyType.RsaHsm) { - options = new CreateRsaKeyOptions(keyName, isHsm) { KeySize = size }; + var options = Track2KeyOptionsFactory.BuildRsaKeyOptions(keyName, isHsm, keyAttributes, size); + return new PSKeyVaultKey(client.CreateRsaKey(options).Value, _vaultUriHelper, false); } - else if (keyAttributes.KeyType == KeyType.Ec || keyAttributes.KeyType == KeyType.EcHsm) + if (keyAttributes.KeyType == KeyType.Ec || keyAttributes.KeyType == KeyType.EcHsm) { - options = new CreateEcKeyOptions(keyName, isHsm) { CurveName = string.IsNullOrEmpty(curveName) ? (KeyCurveName?)null : new KeyCurveName(curveName) }; + var options = Track2KeyOptionsFactory.BuildEcKeyOptions(keyName, isHsm, keyAttributes, curveName); + return new PSKeyVaultKey(client.CreateEcKey(options).Value, _vaultUriHelper, false); } - else + if (keyAttributes.KeyType == KeyType.Oct || keyAttributes.KeyType == KeyType.OctHsm) { - // oct (AES) is only supported by managed HSM - throw new NotSupportedException($"{keyAttributes.KeyType} is not supported"); + // Honor the requested -Destination by deriving hardwareProtected from the + // resolved KeyType (Oct = software, OctHsm = HSM-backed). Software 'oct' + // is not supported on AKV vaults today; in that case the service will + // return the authoritative error rather than us silently promoting the + // request to an HSM-backed create. + bool isOctHsm = keyAttributes.KeyType == KeyType.OctHsm; + var options = Track2KeyOptionsFactory.BuildOctKeyOptions(keyName, isOctHsm, keyAttributes, size); + return new PSKeyVaultKey(client.CreateOctKey(options).Value, _vaultUriHelper, false); } - options.NotBefore = keyAttributes.NotBefore; - options.ExpiresOn = keyAttributes.Expires; - options.Enabled = keyAttributes.Enabled; - options.Exportable = keyAttributes.Exportable; - options.ReleasePolicy = keyAttributes.ReleasePolicy?.ToKeyReleasePolicy(); ; - if (keyAttributes.KeyOps != null) - { - foreach (var keyOp in keyAttributes.KeyOps) - { - options.KeyOperations.Add(new KeyOperation(keyOp)); - } - } - if (keyAttributes.Tags != null) - { - foreach (DictionaryEntry entry in keyAttributes.Tags) - { - options.Tags.Add(entry.Key.ToString(), entry.Value.ToString()); - } - } - - if (keyAttributes.KeyType == KeyType.Rsa || keyAttributes.KeyType == KeyType.RsaHsm) - { - return new PSKeyVaultKey(client.CreateRsaKey(options as CreateRsaKeyOptions).Value, _vaultUriHelper, false); - } - else if (keyAttributes.KeyType == KeyType.Ec || keyAttributes.KeyType == KeyType.EcHsm) - { - return new PSKeyVaultKey(client.CreateEcKey(options as CreateEcKeyOptions).Value, _vaultUriHelper, false); - } - else - { - throw new NotSupportedException($"{keyAttributes.KeyType} is not supported"); - } + throw new NotSupportedException($"{keyAttributes.KeyType} is not supported"); } internal PSKeyOperationResult Decrypt(string vaultName, string keyName, string version, byte[] value, string encryptAlgorithm) diff --git a/src/KeyVault/KeyVault/help/Add-AzKeyVaultKey.md b/src/KeyVault/KeyVault/help/Add-AzKeyVaultKey.md index 4ceef7efcae3..ef262afaa857 100644 --- a/src/KeyVault/KeyVault/help/Add-AzKeyVaultKey.md +++ b/src/KeyVault/KeyVault/help/Add-AzKeyVaultKey.md @@ -423,6 +423,34 @@ Release Policy : Tags : ``` +### Example 11: Create an AES (oct) HSM-protected key in a Premium Azure Key Vault + +```powershell +Add-AzKeyVaultKey -VaultName 'MyPremiumVault' -Name 'aesKey' -KeyType oct -Destination HSM -Size 256 +``` + +```output +Vault/HSM Name : MyPremiumVault +Name : aesKey +Key Type : oct-HSM +Key Size : 256 +Curve Name : +Version : +Id : https://mypremiumvault.vault.azure.net:443/keys/aesKey/ +Enabled : True +Expires : +Not Before : +Created : 1/1/2025 12:00:00 AM +Updated : 1/1/2025 12:00:00 AM +Recovery Level : Recoverable+Purgeable +Tags : +``` + +Creates an AES (symmetric) HSM-protected key named `aesKey` in the Premium-SKU Azure Key Vault `MyPremiumVault`. +The `-Destination HSM` switch is required because symmetric (oct) keys in Azure Key Vault are always HSM-backed, +and the resulting key type is reported as `oct-HSM`. Supported key sizes are 128, 192, and 256 bits. +AES keys are only supported on Key Vault Premium SKU and Managed HSM. + ## PARAMETERS ### -CurveName @@ -773,7 +801,7 @@ Accept wildcard characters: False ``` ### -Size -RSA key size, in bits. If not specified, the service will provide a safe default. +The key size, in bits. For RSA keys, valid values depend on the vault/HSM (for example 2048, 3072, or 4096). For AES (oct/oct-HSM) keys, valid values are 128, 192, or 256. If not specified, the service will provide a safe default. ```yaml Type: System.Nullable`1[System.Int32]