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]