diff --git a/src/KeyVault/KeyVault.Test/KeyVault.Test.csproj b/src/KeyVault/KeyVault.Test/KeyVault.Test.csproj index 07ed849ce7a6..d8d000df5ae5 100644 --- a/src/KeyVault/KeyVault.Test/KeyVault.Test.csproj +++ b/src/KeyVault/KeyVault.Test/KeyVault.Test.csproj @@ -14,6 +14,7 @@ + diff --git a/src/KeyVault/KeyVault.Test/UnitTests/SecurityDomainTests.cs b/src/KeyVault/KeyVault.Test/UnitTests/SecurityDomainTests.cs new file mode 100644 index 000000000000..c7dff9826cce --- /dev/null +++ b/src/KeyVault/KeyVault.Test/UnitTests/SecurityDomainTests.cs @@ -0,0 +1,24 @@ +using Microsoft.Azure.Commands.Common.Authentication.Abstractions; +using Microsoft.Azure.Commands.KeyVault.SecurityDomain; +using Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models; +using System; +using System.Security.Cryptography.X509Certificates; +using Xunit; + +namespace SecurityDomain.Test +{ + public class SecurityDomainTests + { + [Fact] + public void X509Tests() + { + X509Certificate2 cert = new X509Certificate2(@"C:\yeming.liu.cer"); + Assert.NotNull(cert); + + JWK jwk = new JWK(cert); + Assert.NotNull(jwk); + + Assert.Equal(JwkKeyType.RSA.ToString(), jwk.kty); + } + } +} diff --git a/src/KeyVault/KeyVault.sln b/src/KeyVault/KeyVault.sln index dccac43f4a32..f7b226f2fcbc 100644 --- a/src/KeyVault/KeyVault.sln +++ b/src/KeyVault/KeyVault.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27703.2042 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30413.136 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeyVault", "KeyVault\KeyVault.csproj", "{9FFC40CC-A341-4D0C-A25D-DC6B78EF6C94}" EndProject @@ -52,12 +52,17 @@ Global {BC80A1D0-FFA4-43D9-AA74-799F5CB54B58}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC80A1D0-FFA4-43D9-AA74-799F5CB54B58}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC80A1D0-FFA4-43D9-AA74-799F5CB54B58}.Release|Any CPU.Build.0 = Release|Any CPU + {FDEE9611-2887-4933-AF88-B4EC782B2096}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FDEE9611-2887-4933-AF88-B4EC782B2096}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FDEE9611-2887-4933-AF88-B4EC782B2096}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDEE9611-2887-4933-AF88-B4EC782B2096}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {080B0477-7E52-4455-90AB-23BD13D1B1CE} = {95C16AED-FD57-42A0-86C3-2CF4300A4817} + {FDEE9611-2887-4933-AF88-B4EC782B2096} = {95C16AED-FD57-42A0-86C3-2CF4300A4817} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5E85B4CC-D1A9-466B-98AC-E0AD0C5AE585} diff --git a/src/KeyVault/KeyVault/Az.KeyVault.psd1 b/src/KeyVault/KeyVault/Az.KeyVault.psd1 index 399c0e1951cf..bbe7627d564a 100644 --- a/src/KeyVault/KeyVault/Az.KeyVault.psd1 +++ b/src/KeyVault/KeyVault/Az.KeyVault.psd1 @@ -122,7 +122,8 @@ CmdletsToExport = 'Add-AzManagedHsmKey', 'Get-AzManagedHsmKey', 'Remove-AzManage 'Undo-AzKeyVaultManagedStorageSasDefinitionRemoval', 'Undo-AzKeyVaultManagedStorageAccountRemoval', 'Add-AzKeyVaultNetworkRule', 'Update-AzKeyVaultNetworkRuleSet', - 'Remove-AzKeyVaultNetworkRule' + 'Remove-AzKeyVaultNetworkRule', 'Backup-AzManagedHsmSecurityDomain', + 'Restore-AzManagedHsmSecurityDomain' # Variables to export from this module # VariablesToExport = @() diff --git a/src/KeyVault/KeyVault/Helpers/UtilityExtensions.cs b/src/KeyVault/KeyVault/Helpers/UtilityExtensions.cs new file mode 100644 index 000000000000..0ac4c25ac649 --- /dev/null +++ b/src/KeyVault/KeyVault/Helpers/UtilityExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; + +namespace Microsoft.Azure.Commands.KeyVault +{ + internal static class UtilityExtensions + { + public static string ToPlainText(this SecureString secureString) + { + IntPtr bstr = Marshal.SecureStringToBSTR(secureString); + + try + { + return Marshal.PtrToStringBSTR(bstr); + } + finally + { + Marshal.FreeBSTR(bstr); + } + } + } +} diff --git a/src/KeyVault/KeyVault/KeyVault.csproj b/src/KeyVault/KeyVault/KeyVault.csproj index 07ec908b56c6..556c14e346eb 100644 --- a/src/KeyVault/KeyVault/KeyVault.csproj +++ b/src/KeyVault/KeyVault/KeyVault.csproj @@ -1,4 +1,4 @@ - + KeyVault @@ -12,10 +12,12 @@ - + + + diff --git a/src/KeyVault/KeyVault/Models/DataServiceCredential.cs b/src/KeyVault/KeyVault/Models/DataServiceCredential.cs index e3cfe726e612..03805857c1e3 100644 --- a/src/KeyVault/KeyVault/Models/DataServiceCredential.cs +++ b/src/KeyVault/KeyVault/Models/DataServiceCredential.cs @@ -70,12 +70,12 @@ public Task OnAuthentication(string authority, string resource, string s public string GetToken() { - return GetTokenInternal(this.TenantId, this._authenticationFactory, this._context, this._endpointName).Item1.AccessToken; + return GetAccessToken().AccessToken; } - public IAccessToken GetTokenTemp() // todo rename / refactor + public IAccessToken GetAccessToken() { - return GetTokenInternal(this.TenantId, this._authenticationFactory, this._context, this._endpointName).Item1; + return GetTokenInternal(TenantId, _authenticationFactory, _context, _endpointName).Item1; } private static string GetTenantId(IAzureContext context) diff --git a/src/KeyVault/KeyVault/Properties/AssemblyInfo.cs b/src/KeyVault/KeyVault/Properties/AssemblyInfo.cs index ddc0070abd8b..d712573951f0 100644 --- a/src/KeyVault/KeyVault/Properties/AssemblyInfo.cs +++ b/src/KeyVault/KeyVault/Properties/AssemblyInfo.cs @@ -33,4 +33,5 @@ [assembly: AssemblyFileVersion("2.2.1")] #if !SIGN [assembly: InternalsVisibleTo("Microsoft.Azure.PowerShell.Cmdlets.KeyVault.Test")] +[assembly: InternalsVisibleTo("SecurityDomain.Test")] #endif diff --git a/src/KeyVault/KeyVault/Properties/Resources.Designer.cs b/src/KeyVault/KeyVault/Properties/Resources.Designer.cs index 0d7cbfd42369..68089478d334 100644 --- a/src/KeyVault/KeyVault/Properties/Resources.Designer.cs +++ b/src/KeyVault/KeyVault/Properties/Resources.Designer.cs @@ -315,6 +315,24 @@ internal static string CreateKeyVault { } } + /// + /// Looks up a localized string similar to Failed to decrypt security domain data. Please make sure the file is not modified and the keys / passwords are correct.. + /// + internal static string DecryptSecurityDomainFailure { + get { + return ResourceManager.GetString("DecryptSecurityDomainFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not enough keys to decrypt security domain backup. {0} required, {0} provided.. + /// + internal static string DecryptSecurityDomainKeyNotEnough { + get { + return ResourceManager.GetString("DecryptSecurityDomainKeyNotEnough", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot find deleted vault '{0}' in location '{1}'. /// @@ -333,6 +351,24 @@ internal static string DownloadNotSupported { } } + /// + /// Looks up a localized string similar to Failed to download security domain backup data.. + /// + internal static string DownloadSecurityDomainFail { + get { + return ResourceManager.GetString("DownloadSecurityDomainFail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to download security domain exchange key.. + /// + internal static string DownloadSecurityDomainKeyFail { + get { + return ResourceManager.GetString("DownloadSecurityDomainKeyFail", resourceCulture); + } + } + /// /// Looks up a localized string similar to Overwrite File ?. /// @@ -352,6 +388,14 @@ internal static string FileOverwriteMessage { } /// + /// Looks up a localized string similar to To encrypt the security domain data, please provide at least {0} and at most {1} certificates.. + /// + internal static string HsmCertRangeWarning { + get { + return ResourceManager.GetString("HsmCertRangeWarning", resourceCulture); + } + } + /// Looks up a localized string similar to The specified HSM already exists.. /// internal static string HsmAlreadyExists { @@ -684,6 +728,15 @@ internal static string KeyOpsImportIsExclusive { } } + /// + /// Looks up a localized string similar to Failed to load security domain data from {0}. Please make sure the file exists and is not modified.. + /// + internal static string LoadSecurityDomainFileFailed { + get { + return ResourceManager.GetString("LoadSecurityDomainFileFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to There is no default user account associated with this subscription. Certificate accounts are not supported with Azure Key Vault.. /// @@ -1098,6 +1151,33 @@ internal static string RestoreSecret { } } + /// + /// Looks up a localized string similar to "PublicKey" and "PrivateKey" are mandatory properties in each object in "Keys".. + /// + internal static string RestoreSecurityDomainBadKey { + get { + return ResourceManager.GetString("RestoreSecurityDomainBadKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to restore security domain from backup.. + /// + internal static string RestoreSecurityDomainFailure { + get { + return ResourceManager.GetString("RestoreSecurityDomainFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There need to be at least {0} keys to decrypt security domain backup data.. + /// + internal static string RestoreSecurityDomainNotEnoughKey { + get { + return ResourceManager.GetString("RestoreSecurityDomainNotEnoughKey", resourceCulture); + } + } + /// /// Looks up a localized string similar to Set certificate attribute. /// diff --git a/src/KeyVault/KeyVault/Properties/Resources.resx b/src/KeyVault/KeyVault/Properties/Resources.resx index 47768e7475c4..30842c8202e5 100644 --- a/src/KeyVault/KeyVault/Properties/Resources.resx +++ b/src/KeyVault/KeyVault/Properties/Resources.resx @@ -501,6 +501,33 @@ You can find the object ID using Azure Active Directory Module for Windows Power The "import" operation is exclusive, it cannot be combined with any other value(s). + + To encrypt the security domain data, please provide at least {0} and at most {1} certificates. + + + Failed to load security domain data from {0}. Please make sure the file exists and is not modified. + + + "PublicKey" and "PrivateKey" are mandatory properties in each object in "Keys". + + + There need to be at least {0} keys to decrypt security domain backup data. + + + Failed to decrypt security domain data. Please make sure the file is not modified and the keys / passwords are correct. + + + Not enough keys to decrypt security domain backup. {0} required, {0} provided. + + + Failed to download security domain backup data. + + + Failed to download security domain exchange key. + + + Failed to restore security domain from backup. + Invalid key properties diff --git a/src/KeyVault/KeyVault/SecurityDomain/Cmdlets/BackupSecurityDomain.cs b/src/KeyVault/KeyVault/SecurityDomain/Cmdlets/BackupSecurityDomain.cs new file mode 100644 index 000000000000..2a460cab5feb --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Cmdlets/BackupSecurityDomain.cs @@ -0,0 +1,62 @@ +using Microsoft.Azure.Commands.Common.Authentication; +using Microsoft.Azure.Commands.KeyVault.Properties; +using System; +using System.Linq; +using System.Management.Automation; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Cmdlets +{ + [Cmdlet(VerbsData.Backup, ResourceManager.Common.AzureRMConstants.AzurePrefix + "ManagedHsmSecurityDomain", SupportsShouldProcess = true, DefaultParameterSetName = ByName)] + [OutputType(typeof(bool))] + public class BackupSecurityDomain: SecurityDomainCmdlet + { + [Parameter(HelpMessage = "Paths to the certificates that are used to encrypt the security domain data.", Mandatory = true)] + [ValidateNotNullOrEmpty()] + public string[] Certificates { get; set; } + + [Parameter(HelpMessage = "Specify the path where security domain data will be downloaded to.", Mandatory = true)] + [ValidateNotNullOrEmpty] + public string OutputPath { get; set; } + + [Parameter(HelpMessage = "Specify whether to overwrite existing file.")] + public SwitchParameter Force { get; set; } + + [Parameter(HelpMessage = "When specified, a boolean will be returned when cmdlet succeeds.")] + public SwitchParameter PassThru { get; set; } + + [Parameter(HelpMessage = "The minimum number of shares required to decrypt the security domain for recovery.", Mandatory = true)] + [ValidateRange(Common.Constants.MinQuorum, Common.Constants.MaxQuorum)] + public int Quorum { get; set; } + + public override void DoExecuteCmdlet() + { + ValidateParameters(); + + var certificates = Certificates.Select(path => new X509Certificate2(ResolveUserPath(path))); + + if (ShouldProcess($"managed HSM {Name}", $"download encrypted security domain data to '{OutputPath}'")) + { + OutputPath = ResolveUserPath(OutputPath); + var securityDomain = Client.DownloadSecurityDomain(Name, certificates, Quorum); + if (!AzureSession.Instance.DataStore.FileExists(OutputPath) || Force || ShouldContinue(string.Format(Resources.FileOverwriteMessage, OutputPath), Resources.FileOverwriteCaption)) + { + AzureSession.Instance.DataStore.WriteFile(OutputPath, securityDomain); + WriteDebug($"Security domain data of managed HSM '{Name}' downloaded to '{OutputPath}'."); + if (PassThru) + { + WriteObject(true); + } + } + } + } + + private void ValidateParameters() + { + if (Certificates.Length < Common.Constants.MinCert || Certificates.Length > Common.Constants.MaxCert) + { + throw new ArgumentException(string.Format(Resources.HsmCertRangeWarning, Common.Constants.MinCert, Common.Constants.MaxCert)); + } + } + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Cmdlets/RestoreSecurityDomain.cs b/src/KeyVault/KeyVault/SecurityDomain/Cmdlets/RestoreSecurityDomain.cs new file mode 100644 index 000000000000..ba3eb0ff78a3 --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Cmdlets/RestoreSecurityDomain.cs @@ -0,0 +1,79 @@ +using Microsoft.Azure.Commands.KeyVault.Properties; +using Microsoft.Azure.Commands.KeyVault.SecurityDomain.Common; +using Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models; +using Newtonsoft.Json; +using System; +using System.Linq; +using System.Management.Automation; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Cmdlets +{ + [Cmdlet(VerbsData.Restore, ResourceManager.Common.AzureRMConstants.AzurePrefix + "ManagedHsmSecurityDomain", SupportsShouldProcess = true, DefaultParameterSetName = ByName)] + [OutputType(typeof(bool))] + public class RestoreSecurityDomain : SecurityDomainCmdlet + { + [Parameter(HelpMessage = "Information about the keys that are used to decrypt the security domain data. See examples for how it is constructed.", Mandatory = true)] + [ValidateNotNullOrEmpty] + public KeyPath[] Keys { get; set; } + + [Parameter(HelpMessage = "Specify the path to the encrypted security domain data.", Mandatory = true)] + [Alias("Path")] + [ValidateNotNullOrEmpty] + public string SecurityDomainPath { get; set; } + + [Parameter(HelpMessage = "When specified, a boolean will be returned when cmdlet succeeds.")] + public SwitchParameter PassThru { get; set; } + + public override void DoExecuteCmdlet() + { + ValidateParameters(); + if (ShouldProcess($"managed HSM {Name}", $"restore security domain data from file \"{SecurityDomainPath}\"")) + { + Keys = Keys.Select(key => new KeyPath() { + PublicKey = this.ResolveUserPath(key.PublicKey), + PrivateKey = this.ResolveUserPath(key.PrivateKey) + }).ToArray(); + var securityDomain = LoadSdFromFile(SecurityDomainPath); + var rawSecurityDomain = Client.DecryptSecurityDomain(securityDomain, Keys); + var exchangeKey = Client.DownloadSecurityDomainExchangeKey(Name); + var encryptedSecurityDomain = Client.EncryptForRestore(rawSecurityDomain, exchangeKey); + Client.RestoreSecurityDomain(Name, encryptedSecurityDomain); + + if (PassThru) + { + WriteObject(true); + } + } + } + + private void ValidateParameters() + { + if (Keys.Length < 2) + { + throw new ArgumentException(string.Format(Resources.RestoreSecurityDomainNotEnoughKey, Common.Constants.MinQuorum)); + } + if (Keys.Any(key => string.IsNullOrEmpty(key.PublicKey) || string.IsNullOrEmpty(key.PrivateKey))) + { + throw new ArgumentException(Resources.RestoreSecurityDomainBadKey); + } + } + + private SecurityDomainData LoadSdFromFile(string path) + { + try + { + string content = Utils.FileToString(path); + if (string.IsNullOrWhiteSpace(content)) + { + throw new ArgumentException(nameof(SecurityDomainPath)); + } + return JsonConvert.DeserializeObject(content); + } + catch (Exception ex) + { + throw new Exception( + string.Format(Resources.LoadSecurityDomainFileFailed, path), ex); + } + } + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Cmdlets/SecurityDomainCmdlet.cs b/src/KeyVault/KeyVault/SecurityDomain/Cmdlets/SecurityDomainCmdlet.cs new file mode 100644 index 000000000000..c62ce01a98db --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Cmdlets/SecurityDomainCmdlet.cs @@ -0,0 +1,64 @@ +using Microsoft.Azure.Commands.Common.Authentication; +using Microsoft.Azure.Commands.KeyVault.Models; +using Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models; +using Microsoft.Azure.Commands.ResourceManager.Common; +using Microsoft.WindowsAzure.Commands.Utilities.Common; +using System.Management.Automation; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Cmdlets +{ + public abstract class SecurityDomainCmdlet: AzureRMCmdlet + { + protected const string ByName = "ByName"; + protected const string ByInputObject = "ByInputObject"; + protected const string ByResourceId = "ByResourceID"; + + [Parameter(HelpMessage = "Name of the managed HSM.", Mandatory = true, ParameterSetName = ByName)] + [Alias("HsmName")] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + [Parameter(HelpMessage = "Object representing a managed HSM.", Mandatory = true, ParameterSetName = ByInputObject, ValueFromPipeline = true)] + [ValidateNotNull] + public PSKeyVaultIdentityItem InputObject { get; set; } + + internal ISecurityDomainClient Client + { + get + { + if (_client == null) + { + _client = new SecurityDomainClient(AzureSession.Instance.AuthenticationFactory, DefaultContext, s => WriteDebug(s)); + } + return _client; + } + set => _client = value; + } + + + private ISecurityDomainClient _client; + + /// + /// Sub-classes should not override this method, but instead. + /// This is call-super pattern. See https://www.martinfowler.com/bliki/CallSuper.html + /// + public override void ExecuteCmdlet() + { + PreprocessParameterSets(); + DoExecuteCmdlet(); + } + + /// + /// Unifies different parameter sets. Sub-classes need only to care about Name. + /// + private void PreprocessParameterSets() + { + if (this.IsParameterBound(c => c.InputObject)) + { + Name = InputObject.VaultName; + } + } + + public abstract void DoExecuteCmdlet(); + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Common/Constants.cs b/src/KeyVault/KeyVault/SecurityDomain/Common/Constants.cs new file mode 100644 index 000000000000..5ab6f44d4eeb --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Common/Constants.cs @@ -0,0 +1,10 @@ +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Common +{ + internal static class Constants + { + public const int MinQuorum = 2; + public const int MaxQuorum = 10; + public const int MinCert = 3; + public const int MaxCert = 10; + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Common/IOStreamExtensions.cs b/src/KeyVault/KeyVault/SecurityDomain/Common/IOStreamExtensions.cs new file mode 100644 index 000000000000..a6e3fcf66b71 --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Common/IOStreamExtensions.cs @@ -0,0 +1,18 @@ +using System.Security.Cryptography; +using System.IO; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Common +{ + internal static class IOStreamExtensions + { + public static void Write(this MemoryStream stream, byte[] bytes) + { + stream.Write(bytes, 0, bytes.Length); + } + + public static void Write(this CryptoStream stream, byte[] bytes) + { + stream.Write(bytes, 0, bytes.Length); + } + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Common/Utils.cs b/src/KeyVault/KeyVault/SecurityDomain/Common/Utils.cs new file mode 100644 index 000000000000..afacf9e77683 --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Common/Utils.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Common +{ + class Utils + { + static public UInt16[] ConvertToUint16(byte[] b) + { + UInt16[] ret = new UInt16[b.Length / 2]; + + for (Int32 i = 0; i < b.Length; i += 2) + { + byte[] tmp = new byte[2]; + tmp[0] = b[i]; + tmp[1] = b[i + 1]; + + // It's already in the same byte order + // as the system that encrypted it, so don't reverse it + ret[i / 2] = BitConverter.ToUInt16(tmp, 0); + } + + return ret; + + } + static public byte[] Sha256Thumbprint(X509Certificate2 cert) + { + SHA256CryptoServiceProvider hash = new SHA256CryptoServiceProvider(); + return hash.ComputeHash(cert.RawData); + } + + static public string FileToString(string path) + { + string readContents; + using (StreamReader streamReader = new StreamReader(path, Encoding.ASCII)) + { + readContents = streamReader.ReadToEnd(); + } + + return readContents; + } + + static public void StringToFile(string path, string data) + { + using (StreamWriter streamWriter = new StreamWriter(path, false, Encoding.ASCII)) + { + streamWriter.Write(data); + streamWriter.Flush(); + } + } + + public static byte[] GetRandom(UInt32 cb) + { + using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) + { + byte[] random = new byte[cb]; + rng.GetBytes(random); + return random; + } + } + + public static X509Certificate2 CertficateFromPem(string certificatePem) + { + // Remove the header + string base64cert = certificatePem.Replace("-----BEGIN CERTIFICATE-----\n", ""); + + // And tidy up any trailing characters + var footerPosition = base64cert.IndexOf("\n-----END CERTIFICATE"); + X509Certificate2 cert = new X509Certificate2(Convert.FromBase64String(base64cert.Substring(0, footerPosition))); + return cert; + } + } + +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Crypto/mod_math.cs b/src/KeyVault/KeyVault/SecurityDomain/Crypto/mod_math.cs new file mode 100644 index 000000000000..54f30c87206f --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Crypto/mod_math.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Crypto +{ + class mod_math + { + public static UInt32 mod_invert(UInt32 x) + { + UInt32 ret = x; + + for (UInt32 i = 0; i < 7; ++i) + { + ret = mod_multiply(ret, ret); + ret = mod_multiply(ret, x); + } + + return ret; + } + + public static UInt32 mod_reduce(UInt32 x) + { + // Function to find x % 257 without side channels + UInt32 t = (x & 0xff) - (x >> 8); + t += (UInt32)((Int32)t >> 31) & 257; + return t; + } + + // Assumes a, b are within 0-256 + public static UInt32 mod_multiply(UInt32 a, UInt32 b) + { + return mod_reduce(a * b); + } + + public static UInt32 mod_add(UInt32 a, UInt32 b) + { + return mod_reduce(a + b); + } + + public static UInt32 mod_subtract(UInt32 a, UInt32 b) + { + // Must ensure that the difference is in the range of 0-256 + return mod_reduce(a - b + 257); + } + } + + class random_bits + { + public UInt16 get_mod_257() + { + UInt16 tmp = 0; + do + { + tmp = get_word(); + + if (tmp != 0) + return (UInt16)mod_math.mod_reduce(tmp); + + } while (tmp == 0); + + // Not actually reached + return 0; + } + + public UInt16 get_word() + { + Int32 remaining = random_bytes.Length - (Int32)current; + + if (remaining < 2) + load(); + + UInt16 ret = (UInt16)((random_bytes[current+1] << 8) | random_bytes[current]); + current += 2; + return ret; + } + + void load() + { + using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) + { + current = 0; + rng.GetBytes(random_bytes); + } + } + + public random_bits(UInt32 _chunk_size) + { + chunk_size = _chunk_size; + current = 0; + random_bytes = new byte[chunk_size]; + load(); + } + + UInt32 chunk_size; + UInt32 current; + byte[] random_bytes; + } + + struct share + { + public share(UInt16 w) + { + x = (UInt16)(w >> 9); + value = (UInt16)(w & 0x1ff); + } + + public share(UInt16 _x, UInt16 _value) + { + x = _x; + value = _value; + } + + public UInt16 to_uint16() + { + return (UInt16)(x << 9 | value); + } + + public UInt16 x; + public UInt16 value; + } + class shared_math + { + public static UInt16 get_secret(UInt16[] shares, UInt32 size) + { + UInt32 secret = 0; + + // Calculate numerator + for (UInt32 i = 0; i max_shares || required > shares || required < 2) + throw new Exception("Incorrect share or required count"); + + _shares = shares; + _required = required; + _coefficients = new UInt16[required]; + _rand_bits = new random_bits(64); + + } + + public shared_secret(UInt16 required) + { + if (required < 2) + throw new Exception("Incorrect share or required count"); + + _shares = 0; + _required = required; + _rand_bits = new random_bits(64); + } + + public List make_shares(byte[] plaintext) + { + // Output will have size of share count, each share vector will have an entry for every input byte + List share_arrays = new List(); + + for( UInt32 i = 0; i < plaintext.Length; ++i) + { + byte p = plaintext[i]; + UInt16[] share_array = make_shares(p); + + /* + We now have a share created for the total number of shares needed + Each share then needs to be distributed such that there's a share + for each byte of plaintext, effectively transposing the 2-dimensional + array. + */ + Int32 share_count = share_array.Length; + + for (Int32 j = 0; j < share_count; ++j) + { + if (i == 0) + share_arrays.Add(new UInt16[plaintext.Length]); + + UInt16[] current_share_array = share_arrays[j]; + current_share_array[i] = share_array[j]; + } + } + + return share_arrays; + } + + public UInt16[] make_shares(byte secret_byte) + { + UInt16[] share_array = new UInt16[_shares]; + + init_coefficients(); + _coefficients[(UInt32)(_required) - 1] = secret_byte; + + UInt16 x = 1; + for (UInt32 i = 0; i < _shares; ++i, ++x) + { + share s = new share(x, shared_math.make_share(_coefficients, x)); + share_array[i] = s.to_uint16(); + } + + return share_array; + } + + public byte[] get_secret(List share_arrays) + { + byte[] plaintext = new byte[share_arrays[0].Length]; + + if (share_arrays.Count < _required) + { + throw new Exception("Insufficient shares"); + } + + UInt16[] sv = new UInt16[_required]; + + // TODO - all the constants calculated in get_secret + // can be pulled out once and re-used, which will help perf + for (UInt32 j = 0; j < plaintext.Length; ++j) + { + for (Int32 i = 0; i< _required; ++i) + { + UInt16[] sa = share_arrays[i]; + sv[i] = sa[j]; + } + + UInt16 text = get_secret(sv); + plaintext[j] = (byte)(text); + } + + return plaintext; + } + + public UInt16 get_secret(UInt16[] share_array) + { + if (share_array.Length < _required) + throw new Exception("Insufficient shares"); + + return shared_math.get_secret(share_array, _required); + } + + void init_coefficients() + { + for (UInt32 i = 0; i < (UInt32)(_required) - 1; ++i) + { + _coefficients[i] = _rand_bits.get_mod_257(); + } + } + + UInt16 _shares; + UInt16 _required; + UInt16[] _coefficients; + random_bits _rand_bits; + + const UInt32 max_shares = 126; + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Crypto/test/test.cs b/src/KeyVault/KeyVault/SecurityDomain/Crypto/test/test.cs new file mode 100644 index 000000000000..af0156689d95 --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Crypto/test/test.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Crypto.Test +{ + class test + { + static void single_byte_test(byte shares, UInt16 required) + { + for (UInt16 i = 0; i < (UInt16)0x100; ++i) + { + shared_secret secret = new shared_secret(shares, required); + + UInt16[] share_array = secret.make_shares((byte)i); + UInt16 result = secret.get_secret(share_array); + + if (i != result) + { + throw new Exception("single_byte_test failed"); + } + } + } + + static void random_single_byte_test() + { + byte shares = 126; + UInt16 required = 16; + + for (UInt32 i = 0; i < 100000; ++i) + { + // just use i % 256 as the secret + byte secret_value = (byte)(i % 256); + shared_secret secret = new shared_secret(shares, required); + UInt16[] share_array = secret.make_shares(secret_value); + + // Put them into a List so that we can pick them out + List tmp_array = new List(); + + foreach(UInt16 u in share_array) + { + tmp_array.Add(u); + } + + // Now need to randomly pick values + UInt16[] random_shares = new UInt16[required]; + + random_bits bits = new random_bits(required); + + for (UInt32 j = 0; j < required; ++j) + { + // Yes, I really only need a byte, but this is test code + UInt16 r = bits.get_word(); + Int32 pos = (Int32)(r % tmp_array.Count); + + random_shares[j] = tmp_array[pos]; + tmp_array.RemoveAt(pos); + } + + UInt16 result = secret.get_secret(random_shares); + + if (result != secret_value) + { + throw new Exception("random_single_byte_test failed"); + } + } + + Console.WriteLine("random_single_byte_test - success"); + } + + static void test_all_shares() + { + for (UInt16 i = 2; i < 127; ++i) + { + // It will work for larger numbers of required, but + // it takes a while. Can do some targeted tests for large + // share count + UInt16 max_required = 16; + for (UInt16 j = i > max_required ? max_required : i; j > 1; --j) + { + byte shares = (byte)(i); + byte required = (byte)(j); + + single_byte_test(shares, required); + } + } + + Console.WriteLine("test_all_shares - success"); + } + + static void test_126_shares() + { + UInt16 i = 126; + + Console.WriteLine("Running 126 share test"); + + for (UInt16 j = i; j > 1; --j) + { + byte shares = (byte)(i); + byte required = (byte)(j); + + single_byte_test(shares, required); + } + + Console.WriteLine("test_126_shares - success"); + } + + static bool check_result(byte[] a, byte[] b) + { + // Not for cryptographic purposes + // assumes equal lengths + for(Int32 i = 0; i < a.Length; ++i) + { + if (a[i] != b[i]) + return false; + } + return true; + } + + static void test_large_secret() + { + for (UInt32 i = 0; i < 1000000; ++i) + { + shared_secret ss = new shared_secret(11, 5); + + byte[] secret = new byte[32]; + + using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) + { + rng.GetBytes(secret); + } + + List share_arrays = ss.make_shares(secret); + byte[] plaintext = ss.get_secret(share_arrays); + + if ( !check_result(plaintext, secret) ) + { + throw new Exception("test_large_secret failed"); + } + + if ((i + 1) % 100000 == 0) + { + Console.WriteLine("{0} iterations", i + 1); + } + } + + Console.WriteLine("test_large_secret - success"); + } + + static public void run_all_tests() + { + Console.WriteLine("Running single-byte tests"); + Console.WriteLine("Testing 2-126 shares, 2-16 required"); + test_126_shares(); + + Console.WriteLine("\nTesting 126 shares, 2-126 required"); + test_all_shares(); + + Console.WriteLine("\nTesting random shares"); + random_single_byte_test(); + + Console.WriteLine("\nRunning 32 byte secret tests"); + test_large_secret(); + } + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Models/CertKey.cs b/src/KeyVault/KeyVault/SecurityDomain/Models/CertKey.cs new file mode 100644 index 000000000000..e32ec92b308e --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Models/CertKey.cs @@ -0,0 +1,85 @@ +using Microsoft.Azure.Commands.KeyVault.SecurityDomain.Common; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using System; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models +{ + internal class CertKey + { + public void Load(KeyPath path) + { + _cert = new X509Certificate2(path.PublicKey); + RSAParameters parameters = RsaParamsFromPem(path.PrivateKey, path.Password?.ToPlainText()); + _key = RSA.Create(); + _key.ImportParameters(parameters); + _thumbprint = Utils.Sha256Thumbprint(_cert); + } + + public byte[] GetThumbprint() { return _thumbprint; } + public RSA GetKey() { return _key; } + public X509Certificate2 GetCert() { return _cert; } + + static RSAParameters RsaParamsFromPem(string path, string password) + { + using (var stream = File.OpenText(path)) + { + var reader = string.IsNullOrEmpty(password) ? new PemReader(stream) : new PemReader(stream, new PasswordFinder(password)); + var keyParameters = reader.ReadObject() as RsaPrivateCrtKeyParameters; + + return ToRSAParameters(keyParameters); + } + } + + static RSAParameters ToRSAParameters(RsaPrivateCrtKeyParameters privKey) + { + RSAParameters rp = new RSAParameters + { + Modulus = privKey.Modulus.ToByteArrayUnsigned(), + Exponent = privKey.PublicExponent.ToByteArrayUnsigned(), + P = privKey.P.ToByteArrayUnsigned(), + Q = privKey.Q.ToByteArrayUnsigned() + }; + rp.D = ConvertRSAParametersField(privKey.Exponent, rp.Modulus.Length); + rp.DP = ConvertRSAParametersField(privKey.DP, rp.P.Length); + rp.DQ = ConvertRSAParametersField(privKey.DQ, rp.Q.Length); + rp.InverseQ = ConvertRSAParametersField(privKey.QInv, rp.Q.Length); + return rp; + } + + + static byte[] ConvertRSAParametersField(Org.BouncyCastle.Math.BigInteger n, int size) + { + byte[] bs = n.ToByteArrayUnsigned(); + if (bs.Length == size) + return bs; + if (bs.Length > size) + throw new ArgumentException("Specified size too small", "size"); + byte[] padded = new byte[size]; + Array.Copy(bs, 0, padded, size - bs.Length, bs.Length); + return padded; + } + + X509Certificate2 _cert; + RSA _key; + byte[] _thumbprint; + + private class PasswordFinder : IPasswordFinder + { + private readonly string _password; + + public PasswordFinder(string password) + { + _password = password; + } + + public char[] GetPassword() + { + return _password.ToCharArray(); + } + } + } +} \ No newline at end of file diff --git a/src/KeyVault/KeyVault/SecurityDomain/Models/CertKeys.cs b/src/KeyVault/KeyVault/SecurityDomain/Models/CertKeys.cs new file mode 100644 index 000000000000..eae5de3268ec --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Models/CertKeys.cs @@ -0,0 +1,46 @@ +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models +{ + internal class CertKeys + { + public CertKeys() + { + _keys = new Dictionary(); + } + + public void LoadKeys(KeyPath[] paths) + { + foreach (var path in paths) + { + try { LoadKey(path); } + catch (Exception ex) + { + throw new Exception($"Could not load public and private key from {path.PublicKey} and {path.PrivateKey}", ex); + } + } + } + + public void LoadKey(KeyPath path) + { + CertKey certKey = new CertKey(); + certKey.Load(path); + string encodedThumbprint = Base64UrlEncoder.Encode(certKey.GetThumbprint()); + _keys.Add(encodedThumbprint, certKey); + } + + public CertKey Find(string encoded_thumbprint) + { + if (!_keys.TryGetValue(encoded_thumbprint, out CertKey certKey)) + return null; + + return certKey; + } + + public int Count() { return _keys.Count; } + + private readonly Dictionary _keys; + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Models/DownloadRequest.cs b/src/KeyVault/KeyVault/SecurityDomain/Models/DownloadRequest.cs new file mode 100644 index 000000000000..a7657a65db8d --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Models/DownloadRequest.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models +{ + class DownloadRequest + { + public DownloadRequest() + { + Certificates = new List(); + } + + [JsonProperty("required")] + public int Required; + + [JsonProperty("certificates")] + public IList Certificates { get; set; } + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Models/ISecurityDomainClient.cs b/src/KeyVault/KeyVault/SecurityDomain/Models/ISecurityDomainClient.cs new file mode 100644 index 000000000000..64fb66d9eaf4 --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Models/ISecurityDomainClient.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models +{ + public interface ISecurityDomainClient + { + string DownloadSecurityDomain(string hsmName, IEnumerable certificates, int required); + + X509Certificate2 DownloadSecurityDomainExchangeKey(string hsmName); + + PlaintextList DecryptSecurityDomain(SecurityDomainData data, KeyPath[] paths); + + SecurityDomainRestoreData EncryptForRestore(PlaintextList plaintextList, X509Certificate2 cert); + + void RestoreSecurityDomain(string hsmName, SecurityDomainRestoreData securityDomainData); + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Models/JWE.cs b/src/KeyVault/KeyVault/SecurityDomain/Models/JWE.cs new file mode 100644 index 000000000000..b4c6d2a9f149 --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Models/JWE.cs @@ -0,0 +1,321 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Azure.Commands.KeyVault.SecurityDomain.Common; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models +{ + /* + * In the JWE Compact Serialization, a JWE is represented as the +concatenation: + + BASE64URL(UTF8(JWE Protected Header)) || '.' || + BASE64URL(JWE Encrypted Key) || '.' || + BASE64URL(JWE Initialization Vector) || '.' || + BASE64URL(JWE Ciphertext) || '.' || + BASE64URL(JWE Authentication Tag) + */ + + // Sample header = {"alg":"RSA-OAEP-256","enc":"A256CBC-HS512","kid":"not used"} + + public class JWE_header + { + public string alg { get; set; } // algorithm + public string enc { get; set; } // encryption algorithm + public string zip { get; set; } // compression algorithm + public string jku { get; set; } // JWK set URL + public string jwk { get; set; } // JSON Web key + public string kid { get; set; } // Key ID + public string x5u { get; set; } // X509 certificate URL + public string x5c { get; set; } // X509 certificate chain + public string x5t { get; set; } // X.509 Certificate SHA-1 Thumbprint + + [JsonProperty("x5t#S256")] + public string x5t_S256 { get; set; } // X.509 Certificate SHA-256 Thumbprint + public string typ { get; set; } // Type + public string cty { get; set; } // Content type + public string crit { get; set; } // Critical + } + + public class JweDecode + { + public JweDecode(string compact_jwe) + { + string[] parts = compact_jwe.Split('.'); + + if (parts.Length != 5) + { + throw new Exception("Malformed input"); + } + + encoded_header = parts[0]; + string header = Base64UrlEncoder.Decode(encoded_header); + encrypted_key = Base64UrlEncoder.DecodeBytes(parts[1]); + init_vector = Base64UrlEncoder.DecodeBytes(parts[2]); + ciphertext = Base64UrlEncoder.DecodeBytes(parts[3]); + auth_tag = Base64UrlEncoder.DecodeBytes(parts[4]); + + protected_header = JsonConvert.DeserializeObject(header); + } + + // For encryption + public JweDecode() + { + encoded_header = ""; + encrypted_key = null; + init_vector = null; + ciphertext = null; + auth_tag = null; + protected_header = new JWE_header(); + } + + public void EncodeHeader() + { + string header_json = JsonConvert.SerializeObject( + protected_header, + Formatting.None, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + + encoded_header = Base64UrlEncoder.Encode(header_json); + } + + public string EncodeCompact() + { + string ret = encoded_header + "."; + + if (encrypted_key != null) + { + ret += Base64UrlEncoder.Encode(encrypted_key); + } + + ret += "."; + if (init_vector != null) + { + ret += Base64UrlEncoder.Encode(init_vector); + } + + ret += "."; + if (ciphertext != null) + { + ret += Base64UrlEncoder.Encode(ciphertext); + } + + ret += "."; + if (auth_tag != null) + { + ret += Base64UrlEncoder.Encode(auth_tag); + } + + return ret; + } + + public JWE_header protected_header; + public string encoded_header; + public byte[] encrypted_key; + public byte[] init_vector; + public byte[] ciphertext; + public byte[] auth_tag; + } + + public class JWE + { + public JWE(string compact_jwe) + { + jwe_decode = new JweDecode(compact_jwe); + } + + public JWE() + { + jwe_decode = new JweDecode(); + } + + public string EncodeCompact() + { + return jwe_decode.EncodeCompact(); + } + + RSAEncryptionPadding GetPaddingMode() + { + string alg = jwe_decode.protected_header.alg; + switch (alg) + { + case "RSA-OAEP-256": + return RSAEncryptionPadding.OaepSHA256; + case "RSA-OAEP": + return RSAEncryptionPadding.OaepSHA1; + case "RSA1_5": + return RSAEncryptionPadding.Pkcs1; + } + + return null; + } + + public byte[] GetCEK(RSA private_key) + { + return private_key.Decrypt(jwe_decode.encrypted_key, GetPaddingMode()); + } + + public void SetCEK(X509Certificate2 cert, byte[] cek) + { + RSA rsa = cert.GetRSAPublicKey(); + jwe_decode.encrypted_key = rsa.Encrypt(cek, GetPaddingMode()); + } + byte[] DekFromCek(byte[] cek) + { + byte[] dek = new byte[32]; + Array.Copy(cek, 32, dek, 0, 32); + return dek; + } + + byte[] HmacKeyFromCek(byte[] cek) + { + byte[] hk = new byte[32]; + Array.Copy(cek, 0, hk, 0, 32); + return hk; + } + + byte[] GetMac(byte[] hk) + { + HMACSHA512 hMAC = new HMACSHA512(hk); + byte[] header_bytes = Encoding.ASCII.GetBytes(jwe_decode.encoded_header); + + using (MemoryStream stm = new MemoryStream()) + { + UInt64 auth_bits = (UInt64)header_bytes.Length * 8; + stm.Write(header_bytes); + stm.Write(jwe_decode.init_vector); + stm.Write(jwe_decode.ciphertext); + // Add the associated_data_length bytes to the hash + stm.Write(KDF.to_big_endian(auth_bits)); + byte[] hash_data = stm.ToArray(); + + return hMAC.ComputeHash(hash_data); + } + } + + void Aes256HmacSha512Encrypt(byte[] cek, byte[] plain_text) + { + byte[] dek = DekFromCek(cek); + byte[] hk = HmacKeyFromCek(cek); + + using (Aes alg = Aes.Create()) + { + alg.Key = dek; + alg.IV = Utils.GetRandom(16); + ICryptoTransform encryptor = alg.CreateEncryptor(alg.Key, alg.IV); + + using (MemoryStream msEncrypt = new MemoryStream()) + { + using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) + { + csEncrypt.Write(plain_text); + csEncrypt.FlushFinalBlock(); + csEncrypt.Close(); + + // Have to wait to set hash once header is complete + jwe_decode.ciphertext = msEncrypt.ToArray(); + jwe_decode.init_vector = alg.IV; + } + } + } + byte[] mac_value = GetMac(hk); + jwe_decode.auth_tag = new byte[32]; + Array.Copy(mac_value, jwe_decode.auth_tag, 32); + } + + byte[] Aes256HmacSha512Decrypt(byte[] cek) + { + byte[] dek = DekFromCek(cek); + byte[] hk = HmacKeyFromCek(cek); + + byte[] mac_value = GetMac(hk); + // We're then going to truncate the MAC to 32 bytes, as per standard + int test = 0; + for (UInt32 i = 0; i < jwe_decode.auth_tag.Length && jwe_decode.auth_tag.Length == 32; ++i) + { + test |= (jwe_decode.auth_tag[i] ^ mac_value[i]); + } + + if (test != 0) + return null; + + // Nothing has been tampered with, decrypt + using (Aes alg = Aes.Create()) + { + alg.Key = dek; + alg.IV = jwe_decode.init_vector; + ICryptoTransform decryptor = alg.CreateDecryptor(alg.Key, alg.IV); + + using (MemoryStream msDecrypt = new MemoryStream(jwe_decode.ciphertext)) + { + using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) + { + using (BinaryReader srDecrypt = new BinaryReader(csDecrypt)) + { + return srDecrypt.ReadBytes(jwe_decode.ciphertext.Length); + } + } + } + } + } + + // Call this with the last parameter non-null to do direct encryption + public void Encrypt(byte[] cek, byte[] plain_text, string algId, string kid = null) + { + if (kid != null) + { + jwe_decode.protected_header.alg = "dir"; + jwe_decode.protected_header.kid = kid; + } + + switch (algId) + { + case "A256CBC-HS512": + jwe_decode.protected_header.enc = "A256CBC-HS512"; + jwe_decode.EncodeHeader(); + Aes256HmacSha512Encrypt(cek, plain_text); + return; + } + + } + + public byte[] Decrypt(byte[] cek) + { + switch (jwe_decode.protected_header.enc) + { + case "A256CBC-HS512": + return Aes256HmacSha512Decrypt(cek); + } + + return null; + } + + // Note - we don't have a true key wrap, for now + // just encrypt keys as if they were data + public void Encrypt(X509Certificate2 cert, byte[] plaintext) + { + // Only allow one encryption method right now + // and only 256-bit keys + jwe_decode.protected_header.alg = "RSA-OAEP-256"; + jwe_decode.protected_header.kid = "not used"; + + byte[] cek = Utils.GetRandom(64); + + SetCEK(cert, cek); + Encrypt(cek, plaintext, "A256CBC-HS512"); + } + + public byte[] Decrypt(RSA private_key) + { + byte[] cek = GetCEK(private_key); + return Decrypt(cek); + } + + JweDecode jwe_decode; + } + +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Models/JWK.cs b/src/KeyVault/KeyVault/SecurityDomain/Models/JWK.cs new file mode 100644 index 000000000000..c8c365b0db99 --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Models/JWK.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Azure.Commands.KeyVault.SecurityDomain.Common; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models +{ + public enum JwkKeyType + { + RSA // only type supported now + } + public enum JwkUse + { + enc, + sig + } + + /* + (Note that the "key_ops" values intentionally match the "KeyUsage" + values defined in the Web Cryptography API + [W3C.CR-WebCryptoAPI-20141211] specification.) + */ + public enum JwkKeyOps + { + sign, + verify, + encrypt, + decrypt, + wrapKey, + unwrapKey, + deriveKey, + deriveBits + } + + public enum JwkAlg + { + RSA_OAEP, + RSA_OAEP_256 + } + + public class JWK + { + public JWK() + { + key_ops = new List(); + x5c = new List(); + } + + public JWK(X509Certificate2 cert) + { + key_ops = new List(); + x5c = new List(); + + PublicKey publicKey = cert.PublicKey; + + if (publicKey.Key.KeyExchangeAlgorithm != "RSA" || publicKey.Key.KeySize < 2048) + throw new Exception("Invalid argument"); + + RSAParameters rsaParameters = cert.GetRSAPublicKey().ExportParameters(false); + SetExponent(rsaParameters.Exponent); + SetModulus(rsaParameters.Modulus); + + SetKeyType(JwkKeyType.RSA); + + // Figure out the key_ops + // Unsure what deriveKey, deriveBits requires, omit for now + if (cert.PrivateKey != null) + { + AddKeyOp(JwkKeyOps.sign); + AddKeyOp(JwkKeyOps.decrypt); + AddKeyOp(JwkKeyOps.unwrapKey); + } + + AddKeyOp(JwkKeyOps.verify); + AddKeyOp(JwkKeyOps.encrypt); + AddKeyOp(JwkKeyOps.wrapKey); + + SetAlg(JwkAlg.RSA_OAEP_256); + SetX5c(cert); + SetX5t(cert); + SetX5t256(cert); + } + public string ToJson() + { + return JsonConvert.SerializeObject( + this, + Formatting.None, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + } + + void SetExponent(byte[] exp) + { + e = Base64UrlEncoder.Encode(exp); + } + + void SetModulus(byte[] modulus) + { + n = Base64UrlEncoder.Encode(modulus); + } + + void SetKeyType(JwkKeyType keyType) + { + // This is all we support right now + if (keyType == JwkKeyType.RSA) + kty = "RSA"; + else + throw new Exception("Invalid argument"); + } + + void SetUse(JwkUse _use) + { + if (_use == JwkUse.enc) + use = "enc"; + else if (_use == JwkUse.sig) + use = "sig"; + else + throw new Exception("Invalid argument"); + } + + void AddKeyOp(JwkKeyOps keyOp) + { + switch (keyOp) + { + case JwkKeyOps.sign: + key_ops.Add("sign"); + break; + case JwkKeyOps.verify: + key_ops.Add("verify"); + break; + case JwkKeyOps.encrypt: + key_ops.Add("encrypt"); + break; + case JwkKeyOps.decrypt: + key_ops.Add("decrypt"); + break; + case JwkKeyOps.wrapKey: + key_ops.Add("wrapKey"); + break; + case JwkKeyOps.unwrapKey: + key_ops.Add("unwrapKey"); + break; + case JwkKeyOps.deriveKey: + key_ops.Add("deriveKey"); + break; + case JwkKeyOps.deriveBits: + key_ops.Add("deriveBits"); + break; + } + } + + void SetKeyOps(JwkKeyOps[] keyOps) + { + foreach (JwkKeyOps op in keyOps) + { + AddKeyOp(op); + } + } + + public void SetAlg(JwkAlg _alg) + { + if (_alg == JwkAlg.RSA_OAEP) + alg = "RSA-OAEP"; + else + if (_alg == JwkAlg.RSA_OAEP_256) + alg = "RSA-OAEP-256"; + else + throw new Exception("Invalid argument"); + } + + public void SetKid(string _kid) + { + kid = _kid; + } + + void SetX5u() + { + // Just to document that this is not to be used + throw new Exception("Not supported"); + } + + void SetX5c(X509Certificate2 cert) + { + // TODO, note: does not support chain, just one cert + string base64cert = Convert.ToBase64String(cert.RawData); + x5c.Add(base64cert); + } + + public X509Certificate2 GetX5c() + { + if (x5c.Count < 1) + return null; + + string base64cert = x5c[0]; + byte[] rawCert = Convert.FromBase64String(base64cert); + X509Certificate2 cert = new X509Certificate2(rawCert); + return cert; + } + + public string GetX5cAsPem() + { + string base64cert = x5c[0]; + // Convert to PEM + string header = "-----BEGIN CERTIFICATE-----\n"; + string footer = "-----END CERTIFICATE-----"; + string pem = header; + + // Now grab 65-character pieces of the base64-encoded cert + for (Int32 i = 0; i < base64cert.Length; i += 65) + { + Int32 remaining = base64cert.Length - i; + string line = base64cert.Substring(i, remaining > 65 ? 65 : remaining); + pem += line + "\n"; + } + + pem += footer; + return pem; + } + + // TODO - move to utils + public static byte[] ToByteArray(String HexString) + { + int NumberChars = HexString.Length; + byte[] bytes = new byte[NumberChars / 2]; + for (int i = 0; i < NumberChars; i += 2) + { + bytes[i / 2] = Convert.ToByte(HexString.Substring(i, 2), 16); + } + return bytes; + } + + void SetX5t(X509Certificate2 cert) + { + x5t = Base64UrlEncoder.Encode(ToByteArray(cert.Thumbprint)); + } + + void SetX5t256(X509Certificate2 cert) + { + x5t_S256 = Base64UrlEncoder.Encode(Utils.Sha256Thumbprint(cert)); + } + + public string kty { get; set; } + public string use { get; set; } + public IList key_ops { get; set; } + + public string alg { get; set; } + public string kid { get; set; } + public string x5u { get; set; } + public IList x5c { get; set; } + public string x5t { get; set; } // X.509 Certificate SHA-1 Thumbprint + + [JsonProperty("x5t#S256")] + public string x5t_S256 { get; set; } // X.509 Certificate SHA-256 Thumbprint + public string n { get; set; } + public string e { get; set; } + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Models/KDF.cs b/src/KeyVault/KeyVault/SecurityDomain/Models/KDF.cs new file mode 100644 index 000000000000..8a4ded6d9570 --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Models/KDF.cs @@ -0,0 +1,136 @@ +using System; +using System.Text; +using System.Security.Cryptography; +using System.IO; +using System.Linq; +using Microsoft.Azure.Commands.KeyVault.SecurityDomain.Common; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models +{ + internal class KDF + { + static public byte[] to_big_endian(Int32 value) + { + byte[] result = new byte[4]; + result[3] = (byte)((value & 0x000000FF)); + result[2] = (byte)((value & 0x0000FF00) >> 8); + result[1] = (byte)((value & 0x00FF0000) >> 16); + result[0] = (byte)((value & 0xFF000000) >> 24); + return result; + } + static public byte[] to_big_endian(UInt64 value) + { + byte[] result = new byte[8]; + result[7] = (byte)((value & 0x00000000000000FF)); + result[6] = (byte)((value & 0x000000000000FF00) >> 8); + result[5] = (byte)((value & 0x0000000000FF0000) >> 16); + result[4] = (byte)((value & 0x00000000FF000000) >> 24); + result[3] = (byte)((value & 0x000000FF00000000) >> 32); + result[2] = (byte)((value & 0x0000FF0000000000) >> 40); + result[1] = (byte)((value & 0x00FF000000000000) >> 48); + result[0] = (byte)((value & 0xFF00000000000000) >> 56); + return result; + } + + static public bool self_test_sp800_108() + { + string label = "label"; + string context = "context"; + Int32 bitLength = 256; + string hex_result = "f0ca51f6308791404bf68b56024ee7c64d6c737716f81d47e1e68b5c4e399575"; + + byte[] key = Enumerable.Repeat((byte)0x41, 32).ToArray(); + HMACSHA512 hmac = new HMACSHA512(); + + byte[] new_key = sp800_108(key, label, context, hmac, bitLength); + + string hex = BitConverter.ToString(new_key).Replace("-", ""); + + return (hex.ToLower() == hex_result); + } + + // Note - initialize out to be the number of bytes of keying material that you need + // This implements SP 800-108 in counter mode, see section 5.1 + /* + Fixed values: + 1. h - The length of the output of the PRF in bits, and + 2. r - The length of the binary representation of the counter i. + + Input: KI, Label, Context, and L. + + Process: + 1. n := ⎡L/h⎤. + 2. If n > 2^(r-1), then indicate an error and stop. + 3. result(0):= ∅. + 4. For i = 1 to n, do + a. K(i) := PRF (KI, [i]2 || Label || 0x00 || Context || [L]2) + b. result(i) := result(i-1) || K(i). + + 5. Return: KO := the leftmost L bits of result(n). + */ + static public byte[] sp800_108(byte[] key_in, string label, string context, HMAC hMAC, Int32 bit_length) + { + if (bit_length <= 0 || bit_length % 8 != 0) + return null; + + Int32 L = bit_length; + Int32 bytes_needed = bit_length / 8; + Int32 n = 0; + Int32 hash_bits = 0; + + hash_bits = hMAC.HashSize; + + n = L / hash_bits; + + if (L % hash_bits != 0) + n++; + + Int32 hmac_data_size = 4 + label.Length + 1 + context.Length + 4; + byte[] hmac_data_suffix = null; + + using (MemoryStream mem = new MemoryStream()) + { + byte[] zero = new byte[1]; + zero[0] = 0; + + mem.Write(Encoding.UTF8.GetBytes(label)); + mem.Write(zero); + mem.Write(Encoding.UTF8.GetBytes(context)); + mem.Write(to_big_endian(bit_length)); + hmac_data_suffix = mem.ToArray(); + } + + using (MemoryStream out_stm = new MemoryStream()) + { + for (Int32 i = 0; i < n; ++i) + { + byte[] hmac_data = null; + + using (MemoryStream mem = new MemoryStream()) + { + mem.Write(to_big_endian(i + 1)); + mem.Write(hmac_data_suffix); + hmac_data = mem.ToArray(); + } + + hMAC.Key = key_in; + byte[] hash_value = hMAC.ComputeHash(hmac_data); + + if (bytes_needed > hash_value.Length) + { + out_stm.Write(hash_value); + bytes_needed -= hash_value.Length; + } + else + { + out_stm.Write(hash_value, (int)out_stm.Length, bytes_needed); + return out_stm.ToArray(); + } + // reset hmac for next round + hMAC.Initialize(); + } + } + return null; + } + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Models/KeyPath.cs b/src/KeyVault/KeyVault/SecurityDomain/Models/KeyPath.cs new file mode 100644 index 000000000000..f038ed9a24e0 --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Models/KeyPath.cs @@ -0,0 +1,11 @@ +using System.Security; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models +{ + public class KeyPath + { + public string PublicKey { get; set; } + public string PrivateKey { get; set; } + public SecureString Password { get; set; } + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Models/SecurityDomain.cs b/src/KeyVault/KeyVault/SecurityDomain/Models/SecurityDomain.cs new file mode 100644 index 000000000000..df7826236f2c --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Models/SecurityDomain.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models +{ + public class Datum + { + public string compact_jwe { get; set; } + public string tag { get; set; } + } + + public class EncData + { + public EncData() + { + data = new List(); + } + + public IList data { get; set; } + public string kdf { get; set; } + } + + public class Plaintext + { + public byte[] plaintext; + public string tag; + } + + public class PlaintextList + { + public PlaintextList() + { + list = new List(); + } + + public void Add(Plaintext p) + { + list.Add(p); + } + + public List<Plaintext> list; + } + + public class Key + { + public string enc_key { get; set; } + public string x5t_256 { get; set; } + } + + public class KeyPair + { + public Key key1 { get; set; } + public Key key2 { get; set; } + } + + public class SplitKeys + { + public string key_algorithm { get; set; } + public IList<KeyPair> keys { get; set; } + } + + public class SharedKeys + { + public string key_algorithm { get; set; } + public UInt32 required { get; set; } + public IList<Key> enc_shares { get; set; } + } + + public class SecurityDomainData + { + public EncData EncData { get; set; } + + // Because the deserializer isn't very picky, the struct + // can contain both the new and the old members, and we can just use the one we need + public SplitKeys SplitKeys { get; set; } + public SharedKeys SharedKeys { get; set; } + public int version { get; set; } + } + + public class SecurityDomainWrapper + { + public string value { get; set; } + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Models/SecurityDomainClient.cs b/src/KeyVault/KeyVault/SecurityDomain/Models/SecurityDomainClient.cs new file mode 100644 index 000000000000..294398691d7b --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Models/SecurityDomainClient.cs @@ -0,0 +1,446 @@ +using Microsoft.Azure.Commands.Common.Authentication.Abstractions; +using Microsoft.Azure.Commands.KeyVault.Models; +using Microsoft.Azure.Commands.KeyVault.Properties; +using Microsoft.Azure.Commands.KeyVault.SecurityDomain.Common; +using Microsoft.Azure.Commands.KeyVault.SecurityDomain.Crypto; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Rest; +using Microsoft.WindowsAzure.Commands.Utilities.Common; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Authentication; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using static Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureEnvironment; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models +{ + internal class SecurityDomainClient : ServiceClient<SecurityDomainClient>, ISecurityDomainClient + { + public SecurityDomainClient(IAuthenticationFactory authenticationFactory, IAzureContext defaultContext, Action<string> debugWriter) + { + _credentials = new DataServiceCredential(authenticationFactory, defaultContext, ExtendedEndpoint.ManagedHsmServiceEndpointResourceId); + + _uriHelper = new VaultUriHelper( + defaultContext.Environment.GetEndpoint(AzureEnvironment.Endpoint.AzureKeyVaultDnsSuffix), + defaultContext.Environment.GetEndpoint(ExtendedEndpoint.ManagedHsmServiceEndpointSuffix)); + + HttpClient.DefaultRequestHeaders.TransferEncodingChunked = false; + + _writeDebug = debugWriter; + } + + private const string _securityDomainPathFragment = "securitydomain"; + private readonly DataServiceCredential _credentials; + private readonly VaultUriHelper _uriHelper; + private readonly JsonSerializerSettings _serializationSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; + private readonly Action<string> _writeDebug; + + /// <summary> + /// Download security domain data for restore. + /// Data is encrypted with the certificates (public keys) user passes in. + /// </summary> + /// <param name="hsmName">Name of the HSM</param> + /// <param name="certificates">Certificates used to encrypt the security domain data</param> + /// <param name="quorum">Specify how many keys are required to decrypt the data</param> + /// <returns>Encrypted HSM security domain data in string</returns> + public string DownloadSecurityDomain(string hsmName, IEnumerable<X509Certificate2> certificates, int quorum) + { + var downloadRequest = new DownloadRequest + { + Required = quorum + }; + certificates.ForEach(cert => downloadRequest.Certificates.Add(new JWK(cert))); + + string requestBody = JsonConvert.SerializeObject( + downloadRequest, + Formatting.None, + _serializationSettings); + + var httpRequest = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new UriBuilder(_uriHelper.CreateManagedHsmUri(hsmName)) + { + Path = $"/{_securityDomainPathFragment}/download" + }.Uri, + Content = new StringContent(requestBody) + }; + + PrepareRequest(httpRequest); + + var httpResponseMessage = HttpClient.SendAsync(httpRequest).ConfigureAwait(false).GetAwaiter().GetResult(); + + if (httpResponseMessage.IsSuccessStatusCode) + { + string response = httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + var securityDomainWrapper = JsonConvert.DeserializeObject<SecurityDomainWrapper>(response); + ValidateDownloadSecurityDomainResponse(securityDomainWrapper); + return securityDomainWrapper.value; + } + else + { + string response = httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + _writeDebug($"Invalid security domain response: {response}"); + throw new Exception(Resources.DownloadSecurityDomainFail); + } + } + + private void ValidateDownloadSecurityDomainResponse(SecurityDomainWrapper securityDomainWrapper) + { + if (string.IsNullOrEmpty(securityDomainWrapper.value) || !ValidateSecurityDomainData(securityDomainWrapper.value)) + { + _writeDebug($"Invalid security domain response: {securityDomainWrapper.value}"); + throw new Exception(Resources.DownloadSecurityDomainFail); + } + } + + /// <summary> + /// Prepare common headers for the request. + /// Such as content-type and authorization. + /// </summary> + /// <param name="httpRequest"></param> + private void PrepareRequest(HttpRequestMessage httpRequest) + { + if (httpRequest.Content != null) + { + httpRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); + } + + try + { + var token = _credentials.GetAccessToken(); + token.AuthorizeRequest((tokenType, tokenValue) => + { + httpRequest.Headers.Authorization = new AuthenticationHeaderValue(tokenType, tokenValue); + }); + } + catch (Exception ex) + { + throw new AuthenticationException(Resources.InvalidSubscriptionState, ex); + } + } + + private bool ValidateSecurityDomainData(string securityDomainData) + { + var securityDomain = JsonConvert.DeserializeObject<SecurityDomainData>(securityDomainData); + + // DeserializeObject isn't very picky, need to validate further + bool valid = false; + + // Note - this is very rudimentary, should + // do more comprehensive checking. + if (securityDomain.EncData != null) + { + switch (securityDomain.version) + { + case 1: + if (securityDomain.SplitKeys != null) + valid = true; + break; + + case 2: + if (securityDomain.SharedKeys != null) + valid = true; + break; + + default: + break; + } + } + + return valid; + } + + /// <summary> + /// Download a security domain exchange key. + /// This key is used to encrypt SD data before uploading to the HSM where SD is going to be restored. + /// </summary> + /// <param name="hsmName"></param> + /// <returns></returns> + public X509Certificate2 DownloadSecurityDomainExchangeKey(string hsmName) + { + try + { + var httpRequest = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new UriBuilder(_uriHelper.CreateManagedHsmUri(hsmName)) + { + Path = $"/{_securityDomainPathFragment}/upload" + }.Uri, + }; + + PrepareRequest(httpRequest); + + HttpResponseMessage httpResponseMessage = HttpClient.SendAsync(httpRequest).ConfigureAwait(false).GetAwaiter().GetResult(); + + if (httpResponseMessage.IsSuccessStatusCode) + { + var response = httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + var key = JsonConvert.DeserializeObject<SecurityDomainTransferKey>(response); + + switch (key.KeyFormat) + { + case "pem": + // Transitional, remove later + return Utils.CertficateFromPem(key.TransferKey); + case "jwk": + // handle below + break; + default: + throw new Exception($"Unexpected key type {key.KeyFormat}"); + } + + // The transfer key is a JWK, need to parse it, and return the cert + JWK jwk = JsonConvert.DeserializeObject<JWK>(key.TransferKey); + return Utils.CertficateFromPem(jwk.GetX5cAsPem()); + } + else + { + string response = httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + _writeDebug($"Invalid security domain response: {response}"); + throw new Exception(Resources.DownloadSecurityDomainKeyFail); + } + + } + catch (Exception ex) + { + throw new Exception(Resources.DownloadSecurityDomainKeyFail, ex); + } + } + + /// <summary> + /// Decrypt security domain data. + /// User must specify public key / private key / password* groups to decrypt SD. + /// *password MAY be optional. + /// </summary> + /// <param name="data"></param> + /// <param name="paths"></param> + /// <returns></returns> + public PlaintextList DecryptSecurityDomain(SecurityDomainData data, KeyPath[] paths) + { + CertKeys certKeys = new CertKeys(); + try + { + certKeys.LoadKeys(paths); + return Decrypt(data, certKeys); + } + catch (Exception ex) + { + throw new Exception(Resources.DecryptSecurityDomainFailure, ex); + } + } + + // Internal worker function + private PlaintextList Decrypt(SecurityDomainData data, CertKeys certKeys) + { + if (data.version == 2 && certKeys.Count() < data.SharedKeys.required) + { + throw new ArgumentException(string.Format(Resources.DecryptSecurityDomainKeyNotEnough, data.SharedKeys.required, certKeys.Count())); + } + + byte[] masterKey; + if (data.version == 1) + { + // ensure that the key splitting algorithm + // is known, currently only one we know about + if (data.SplitKeys.key_algorithm != "xor_split") + { + throw new Exception($"Unknown SplitKey algorithm {data.SplitKeys.key_algorithm}."); + } + + KeyPair decodeKeyPair = null; + CertKey certKey1 = null; + CertKey certKey2 = null; + foreach (KeyPair keyPair in data.SplitKeys.keys) + { + certKey1 = certKeys.Find(keyPair.key1.x5t_256); + + if (certKey1 == null) + continue; + + certKey2 = certKeys.Find(keyPair.key2.x5t_256); + + if (certKey2 != null) + { + decodeKeyPair = keyPair; + break; + } + } + + if (decodeKeyPair == null) + { + throw new Exception("Cannot find matching certs and keys for security domain"); + } + + masterKey = DecryptMasterKey(decodeKeyPair, certKey1, certKey2); + } + else if (data.version == 2) + { + if (data.SharedKeys.key_algorithm != "shamir_share") + { + throw new Exception($"Unknown SharedKeys algorithm {data.SharedKeys.key_algorithm}"); + } + + UInt32 shares_found = 0; + List<UInt16[]> share_arrays = new List<UInt16[]>(); + + foreach (Key key in data.SharedKeys.enc_shares) + { + CertKey cert_key = certKeys.Find(key.x5t_256); + + if (cert_key != null) + { + JWE jwe = new JWE(key.enc_key); + byte[] share = jwe.Decrypt(cert_key.GetKey()); + + shares_found++; + share_arrays.Add(Utils.ConvertToUint16(share)); + } + + if (share_arrays.Count == data.SharedKeys.required) + break; + } + + if (share_arrays.Count < data.SharedKeys.required) + { + throw new Exception($"Insufficient shares available. {data.SharedKeys.required} required, got {share_arrays.Count}."); + } + + shared_secret secret = new shared_secret((UInt16)data.SharedKeys.required); + masterKey = secret.get_secret(share_arrays); + } + else + { + throw new Exception($"Unknown domain version {data.version}."); + } + + PlaintextList plaintextList = new PlaintextList(); + + // Need to check KDF + foreach (Datum enc_data in data.EncData.data) + { + Plaintext p = new Plaintext(); + HMACSHA512 hmac = new HMACSHA512(); + byte[] enc_key = KDF.sp800_108(masterKey, enc_data.tag, "", hmac, 512); + JWE jwe_data = new JWE(enc_data.compact_jwe); + p.plaintext = jwe_data.Decrypt(enc_key); + p.tag = enc_data.tag; + + plaintextList.Add(p); + } + + return plaintextList; + } + + private byte[] DecryptMasterKey(KeyPair decode_key_pair, CertKey certKey1, CertKey certKey2) + { + JWE jwe1 = new JWE(decode_key_pair.key1.enc_key); + byte[] xor_key = jwe1.Decrypt(certKey1.GetKey()); + + JWE jwe2 = new JWE(decode_key_pair.key2.enc_key); + byte[] derived_key = jwe2.Decrypt(certKey2.GetKey()); + + // Now, XOR to get the master key back + byte[] master_key = new byte[xor_key.Length]; + + for (Int32 i = 0; i < xor_key.Length; ++i) + { + master_key[i] = (byte)(xor_key[i] ^ derived_key[i]); + } + return master_key; + } + + /// <summary> + /// Encrypt SD data with exchange key. + /// </summary> + /// <param name="plaintextList"></param> + /// <param name="cert">Exchange key</param> + /// <returns></returns> + public SecurityDomainRestoreData EncryptForRestore(PlaintextList plaintextList, X509Certificate2 cert) + { + try + { + SecurityDomainRestoreData securityDomainRestoreData = new SecurityDomainRestoreData(); + securityDomainRestoreData.EncData.kdf = "sp108_kdf"; + + byte[] master_key = Utils.GetRandom(32); + + foreach (Plaintext p in plaintextList.list) + { + Datum datum = new Datum(); + HMACSHA512 hmac = new HMACSHA512(); + byte[] enc_key = KDF.sp800_108(master_key, p.tag, "", hmac, 512); + + datum.tag = p.tag; + JWE jwe = new JWE(); + jwe.Encrypt(enc_key, p.plaintext, "A256CBC-HS512", p.tag); + datum.compact_jwe = jwe.EncodeCompact(); + securityDomainRestoreData.EncData.data.Add(datum); + } + + // Now go make the wrapped key + JWE jwe_wrapped = new JWE(); + jwe_wrapped.Encrypt(cert, master_key); + securityDomainRestoreData.WrappedKey.enc_key = jwe_wrapped.EncodeCompact(); + securityDomainRestoreData.WrappedKey.x5t_256 = Base64UrlEncoder.Encode(Utils.Sha256Thumbprint(cert)); + return securityDomainRestoreData; + } + catch (Exception ex) + { + throw new Exception("Failed to encrypt security domain data for restoring.", ex); + } + + } + + /// <summary> + /// Upload security domain data and initiate restoring. + /// </summary> + /// <param name="hsmName"></param> + /// <param name="securityDomainData">Encrypted by exchange key</param> + public void RestoreSecurityDomain(string hsmName, SecurityDomainRestoreData securityDomainData) + { + string securityDomain = JsonConvert.SerializeObject(new SecurityDomainWrapper + { + value = JsonConvert.SerializeObject(securityDomainData) + }); + + try + { + var httpRequest = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new UriBuilder(_uriHelper.CreateManagedHsmUri(hsmName)) + { + Path = $"/{_securityDomainPathFragment}/upload" + }.Uri, + Content = new StringContent(securityDomain) + }; + + PrepareRequest(httpRequest); + + var httpResponseMessage = HttpClient.SendAsync(httpRequest).ConfigureAwait(false).GetAwaiter().GetResult(); + var responseBody = httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + if (httpResponseMessage.IsSuccessStatusCode) + { + if (string.IsNullOrEmpty(responseBody)) + { + throw new Exception("Got empty response when restoring security domain."); + } + } + else + { + throw new Exception($"Got {httpResponseMessage.StatusCode}, {responseBody}"); + } + } + catch (Exception ex) + { + throw new Exception(Resources.RestoreSecurityDomainFailure, ex); + } + } + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Models/SecurityDomainRestoreData.cs b/src/KeyVault/KeyVault/SecurityDomain/Models/SecurityDomainRestoreData.cs new file mode 100644 index 000000000000..f2036055fcc7 --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Models/SecurityDomainRestoreData.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models +{ + public class SecurityDomainRestoreData + { + public SecurityDomainRestoreData() + { + EncData = new EncData(); + WrappedKey = new Key(); + } + public EncData EncData { get; set; } + public Key WrappedKey { get; set; } + } +} diff --git a/src/KeyVault/KeyVault/SecurityDomain/Models/SecurityDomainTransferKey.cs b/src/KeyVault/KeyVault/SecurityDomain/Models/SecurityDomainTransferKey.cs new file mode 100644 index 000000000000..e251b04ab857 --- /dev/null +++ b/src/KeyVault/KeyVault/SecurityDomain/Models/SecurityDomainTransferKey.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models +{ + public class SecurityDomainTransferKey + { + [JsonProperty("transfer_key")] + public string TransferKey { get; set; } + + [JsonProperty("key_format")] + public string KeyFormat { get; set; } + } +} diff --git a/src/KeyVault/KeyVault/Track2Models/Track2VaultClient.cs b/src/KeyVault/KeyVault/Track2Models/Track2VaultClient.cs index 9cd484037069..9b9b94fd2cd1 100644 --- a/src/KeyVault/KeyVault/Track2Models/Track2VaultClient.cs +++ b/src/KeyVault/KeyVault/Track2Models/Track2VaultClient.cs @@ -3,8 +3,6 @@ using Microsoft.Azure.Commands.KeyVault.Models; using System; using System.Collections; -using System.Collections.Generic; -using System.Text; namespace Microsoft.Azure.Commands.KeyVault.Track2Models { diff --git a/src/KeyVault/KeyVault/help/Az.KeyVault.md b/src/KeyVault/KeyVault/help/Az.KeyVault.md index 37332c13328c..2cea498efdab 100644 --- a/src/KeyVault/KeyVault/help/Az.KeyVault.md +++ b/src/KeyVault/KeyVault/help/Az.KeyVault.md @@ -41,6 +41,9 @@ Backs up a KeyVault-managed storage account. ### [Backup-AzKeyVaultSecret](Backup-AzKeyVaultSecret.md) Backs up a secret in a key vault. +### [Backup-AzManagedHsmSecurityDomain](Backup-AzManagedHsmSecurityDomain.md) +Backs up the security domain data of a managed HSM for restoring. + ### [Backup-AzManagedHsmKey](Backup-AzManagedHsmKey.md) Backs up a key in a managed HSM. @@ -152,6 +155,9 @@ Restores a managed storage account in a key vault from a backup file. ### [Restore-AzKeyVaultSecret](Restore-AzKeyVaultSecret.md) Creates a secret in a key vault from a backed-up secret. +### [Restore-AzManagedHsmSecurityDomain](Restore-AzManagedHsmSecurityDomain.md) +Restores previous backed up security domain data to a managed HSM. + ### [Restore-AzManagedHsmKey](Restore-AzManagedHsmKey.md) Creates a key in a managed HSM from a backed-up key. diff --git a/src/KeyVault/KeyVault/help/Backup-AzManagedHsmSecurityDomain.md b/src/KeyVault/KeyVault/help/Backup-AzManagedHsmSecurityDomain.md new file mode 100644 index 000000000000..050b2998e243 --- /dev/null +++ b/src/KeyVault/KeyVault/help/Backup-AzManagedHsmSecurityDomain.md @@ -0,0 +1,208 @@ +--- +external help file: Microsoft.Azure.PowerShell.Cmdlets.KeyVault.dll-Help.xml +Module Name: Az.KeyVault +online version: https://docs.microsoft.com/en-us/powershell/module/az.keyvault/backup-azmanagedhsmsecuritydomain +schema: 2.0.0 +--- + +# Backup-AzManagedHsmSecurityDomain + +## SYNOPSIS +Backs up the security domain data of a managed HSM for restoring. + +## SYNTAX + +### By Name (Default) +``` +Backup-AzManagedHsmSecurityDomain -Certificates <String[]> -OutputPath <String> [-Force] [-PassThru] + -Quorum <Int32> -Name <String> [-DefaultProfile <IAzureContextContainer>] [-WhatIf] [-Confirm] + [<CommonParameters>] +``` + +### By InputObject +``` +Backup-AzManagedHsmSecurityDomain -Certificates <String[]> -OutputPath <String> [-Force] [-PassThru] + -Quorum <Int32> -InputObject <PSKeyVaultIdentityItem> [-DefaultProfile <IAzureContextContainer>] [-WhatIf] + [-Confirm] [<CommonParameters>] +``` + +## DESCRIPTION +This cmdlet backs up the security domain data of a managed HSM for restoring. + +## EXAMPLES + +### Example 1 +```powershell +PS C:\Users\username\> Backup-AzManagedHsmSecurityDomain -Name testmhsm -Certificates {pathOfCertificates}/sd1.cer, {pathOfCertificates}/sd2.cer, {pathOfCertificates}/sd3.cer -OutputPath {pathOfOutput}/sd.ps.json -Quorum 2 + +``` + +This command retrieves the managed HSM named testmhsm and saves a backup of that managed HSM security domain to the specified output file. + +## PARAMETERS + +### -Certificates +Paths to the certificates that are used to encrypt the security domain data. + +```yaml +Type: System.String[] +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DefaultProfile +The credentials, account, tenant, and subscription used for communication with Azure. + +```yaml +Type: Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core.IAzureContextContainer +Parameter Sets: (All) +Aliases: AzContext, AzureRmContext, AzureCredential + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force +Specify whether to overwrite existing file. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InputObject +Object representing a managed HSM. + +```yaml +Type: Microsoft.Azure.Commands.KeyVault.Models.PSKeyVaultIdentityItem +Parameter Sets: By InputObject +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -Name +Name of the managed HSM. + +```yaml +Type: System.String +Parameter Sets: By Name +Aliases: HsmName + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -OutputPath +Specify the path where security domain data will be downloaded to. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PassThru +When specified, a boolean will be returned when cmdlet succeeds. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Quorum +The minimum number of shares required to decrypt the security domain for recovery. + +```yaml +Type: System.Int32 +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Confirm +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhatIf +Shows what would happen if the cmdlet runs. +The cmdlet is not run. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### Microsoft.Azure.Commands.KeyVault.Models.PSKeyVaultIdentityItem + +## OUTPUTS + +### System.Boolean + +## NOTES + +## RELATED LINKS diff --git a/src/KeyVault/KeyVault/help/New-AzKeyVault.md b/src/KeyVault/KeyVault/help/New-AzKeyVault.md index 21cea2ed8011..3e798dfb8f73 100644 --- a/src/KeyVault/KeyVault/help/New-AzKeyVault.md +++ b/src/KeyVault/KeyVault/help/New-AzKeyVault.md @@ -13,6 +13,7 @@ Creates a key vault. ## SYNTAX +### KeyVaultParameterSet (Default) ``` New-AzKeyVault [-Name] <String> [-ResourceGroupName] <String> [-Location] <String> [-EnabledForDeployment] [-EnabledForTemplateDeployment] [-EnabledForDiskEncryption] [-EnablePurgeProtection] diff --git a/src/KeyVault/KeyVault/help/Restore-AzManagedHsmSecurityDomain.md b/src/KeyVault/KeyVault/help/Restore-AzManagedHsmSecurityDomain.md new file mode 100644 index 000000000000..abfb98317f7b --- /dev/null +++ b/src/KeyVault/KeyVault/help/Restore-AzManagedHsmSecurityDomain.md @@ -0,0 +1,179 @@ +--- +external help file: Microsoft.Azure.PowerShell.Cmdlets.KeyVault.dll-Help.xml +Module Name: Az.KeyVault +online version: https://docs.microsoft.com/en-us/powershell/module/az.keyvault/restore-azmanagedhsmsecuritydomain +schema: 2.0.0 +--- + +# Restore-AzManagedHsmSecurityDomain + +## SYNOPSIS +Restores previous backed up security domain data to a managed HSM. + +## SYNTAX + +### By Name (Default) +``` +Restore-AzManagedHsmSecurityDomain -Keys <KeyPath[]> -SecurityDomainPath <String> [-PassThru] -Name <String> + [-DefaultProfile <IAzureContextContainer>] [-WhatIf] [-Confirm] [<CommonParameters>] +``` + +### By InputObject +``` +Restore-AzManagedHsmSecurityDomain -Keys <KeyPath[]> -SecurityDomainPath <String> [-PassThru] + -InputObject <PSKeyVaultIdentityItem> [-DefaultProfile <IAzureContextContainer>] [-WhatIf] [-Confirm] + [<CommonParameters>] +``` + +## DESCRIPTION +This cmdlet restores previous backed up security domain data to a managed HSM. + +## EXAMPLES + +### Example 1 +```powershell +PS C:\> $keys = @{PublicKey = "sd1.cer"; PrivateKey = "sd1.key"}, @{PublicKey = "sd2.cer"; PrivateKey = sd2.key"}, @{PublicKey = "sd3.cer"; PrivateKey = "sd3.key"} +PS C:\> Restore-AzManagedHsmSecurityDomain -Name testmhsm -Keys $keys -SecurityDomainPath {pathOfBackup}\sd.ps.json + +``` +First, the keys need be provided to decrypt the security domain data. +Then, The **Restore-AzManagedHsmSecurityDomain** command restores previous backed up security domain data to a managed HSM using these keys. + +## PARAMETERS + +### -DefaultProfile +The credentials, account, tenant, and subscription used for communication with Azure. + +```yaml +Type: Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core.IAzureContextContainer +Parameter Sets: (All) +Aliases: AzContext, AzureRmContext, AzureCredential + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InputObject +Object representing a managed HSM. + +```yaml +Type: Microsoft.Azure.Commands.KeyVault.Models.PSKeyVaultIdentityItem +Parameter Sets: By InputObject +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -Keys +Information about the keys that are used to decrypt the security domain data. +See examples for how it is constructed. + +```yaml +Type: Microsoft.Azure.Commands.KeyVault.SecurityDomain.Models.KeyPath[] +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Name +Name of the managed HSM. + +```yaml +Type: System.String +Parameter Sets: By Name +Aliases: HsmName + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PassThru +When specified, a boolean will be returned when cmdlet succeeds. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -SecurityDomainPath +Specify the path to the encrypted security domain data. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: Path + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Confirm +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhatIf +Shows what would happen if the cmdlet runs. +The cmdlet is not run. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### Microsoft.Azure.Commands.KeyVault.Models.PSKeyVaultIdentityItem + +## OUTPUTS + +### System.Boolean + +## NOTES + +## RELATED LINKS diff --git a/tools/StaticAnalysis/Exceptions/Az.KeyVault/BreakingChangeIssues.csv b/tools/StaticAnalysis/Exceptions/Az.KeyVault/BreakingChangeIssues.csv index 9bfad0acd7c1..fffc239a8835 100644 --- a/tools/StaticAnalysis/Exceptions/Az.KeyVault/BreakingChangeIssues.csv +++ b/tools/StaticAnalysis/Exceptions/Az.KeyVault/BreakingChangeIssues.csv @@ -1,4 +1,5 @@ "AssemblyFileName","ClassName","Target","Severity","ProblemId","Description","Remediation" + "Microsoft.Azure.PowerShell.Cmdlets.KeyVault.dll","Microsoft.Azure.Commands.KeyVault.NewAzureKeyVault","New-AzKeyVault","0","2020","The cmdlet 'New-AzKeyVault' no longer supports the type 'Microsoft.Azure.Management.KeyVault.Models.SkuName' for parameter 'Sku'.","Change the type for parameter 'Sku' back to 'Microsoft.Azure.Management.KeyVault.Models.SkuName'." "Microsoft.Azure.PowerShell.Cmdlets.KeyVault.dll","Microsoft.Azure.Commands.KeyVault.NewAzureKeyVault","New-AzKeyVault","0","1050","The parameter set '__AllParameterSets' for cmdlet 'New-AzKeyVault' has been removed.","Add parameter set '__AllParameterSets' back to cmdlet 'New-AzKeyVault'." "Microsoft.Azure.PowerShell.Cmdlets.KeyVault.dll","Microsoft.Azure.Commands.KeyVault.UpdateTopLevelResourceCommand","Update-AzKeyVault","0","1050","The parameter set 'UpdateByNameParameterSet' for cmdlet 'Update-AzKeyVault' has been removed.","Add parameter set 'UpdateByNameParameterSet' back to cmdlet 'Update-AzKeyVault'." diff --git a/tools/StaticAnalysis/Exceptions/Az.KeyVault/SignatureIssues.csv b/tools/StaticAnalysis/Exceptions/Az.KeyVault/SignatureIssues.csv index d01bd03ea014..c77dde4efec1 100644 --- a/tools/StaticAnalysis/Exceptions/Az.KeyVault/SignatureIssues.csv +++ b/tools/StaticAnalysis/Exceptions/Az.KeyVault/SignatureIssues.csv @@ -1,4 +1,6 @@ "AssemblyFileName","ClassName","Target","Severity","ProblemId","Description","Remediation" +"Microsoft.Azure.PowerShell.Cmdlets.KeyVault.dll","Microsoft.Azure.Commands.KeyVault.SecurityDomain.Cmdlets.BackupSecurityDomain","Backup-AzManagedHsmSecurityDomain","1","8410","Parameter Certificates of cmdlet Backup-AzManagedHsmSecurityDomain does not follow the enforced naming convention of using a singular noun for a parameter name.","Consider using a singular noun for the parameter name." +"Microsoft.Azure.PowerShell.Cmdlets.KeyVault.dll","Microsoft.Azure.Commands.KeyVault.SecurityDomain.Cmdlets.RestoreSecurityDomain","Restore-AzManagedHsmSecurityDomain","1","8410","Parameter Keys of cmdlet Restore-AzManagedHsmSecurityDomain does not follow the enforced naming convention of using a singular noun for a parameter name.","Consider using a singular noun for the parameter name." "Microsoft.Azure.PowerShell.Cmdlets.KeyVault.dll","Microsoft.Azure.Commands.KeyVault.GetAzureManagedHsmKey","Get-AzManagedHsmKey","1","8410","Parameter IncludeVersions of cmdlet Get-AzManagedHsmKey does not follow the enforced naming convention of using a singular noun for a parameter name.","Consider using a singular noun for the parameter name." "Microsoft.Azure.PowerShell.Cmdlets.KeyVault.dll","Microsoft.Azure.Commands.KeyVault.UpdateAzureManagedHsmKey","Update-AzManagedHsmKey","1","8410","Parameter Expires of cmdlet Update-AzManagedHsmKey does not follow the enforced naming convention of using a singular noun for a parameter name.","Consider using a singular noun for the parameter name." "Microsoft.Azure.PowerShell.Cmdlets.KeyVault.dll","Microsoft.Azure.Commands.KeyVault.UpdateAzureManagedHsmKey","Update-AzManagedHsmKey","1","8410","Parameter KeyOps of cmdlet Update-AzManagedHsmKey does not follow the enforced naming convention of using a singular noun for a parameter name.","Consider using a singular noun for the parameter name."