diff --git a/src/KeyVault/KeyVault.Test/PesterTests/Certificate.Tests.ps1 b/src/KeyVault/KeyVault.Test/PesterTests/Certificate.Tests.ps1 new file mode 100644 index 000000000000..23332075323f --- /dev/null +++ b/src/KeyVault/KeyVault.Test/PesterTests/Certificate.Tests.ps1 @@ -0,0 +1,43 @@ +BeforeAll { + $vaultName = 'nori-kv765' + . "..\Scripts\Common.ps1" +} + +Describe "Import Certificate with policy" { + It "ImportCertificateFromFileWithPolicyParameterSet" { + $certName = Get-CertificateName + $certFilePath = "..\Resources\importCertWithPolicy.pfx" + $policy = New-AzKeyVaultCertificatePolicy -SecretContentType "application/x-pkcs12" -SubjectName "CN=contoso.com" -IssuerName "Self" -ValidityInMonths 6 -ReuseKeyOnRenewal + + $cert = Import-AzKeyVaultCertificate -VaultName $vaultName -Name $certName -FilePath $certFilePath -PolicyObject $policy + $cert.Policy.SecretContentType | Should -Be "application/x-pkcs12" + } + It "ImportCertificateFromFileWithPolicyFileParameterSet" { + $certName = Get-CertificateName + $certFilePath = "..\Resources\importCertWithPolicy.pfx" + $policyPath = "..\Resources\certPolicy.json" + + $cert = Import-AzKeyVaultCertificate -VaultName $vaultName -Name $certName -FilePath $certFilePath -PolicyPath $policyPath + $cert.Policy.SecretContentType | Should -Be "application/x-pkcs12" + } + It "ImportWithPrivateKeyFromStringWithPolicyFileParameterSet" { + $certName = Get-CertificateName + $certFilePath = "..\Resources\importCertWithPolicy.pfx" + $policyPath = "..\Resources\certPolicy.json" + $Base64StringCertificate = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($certFilePath)) + + $cert = Import-AzKeyVaultCertificate -VaultName $vaultName -Name $certName -CertificateString $Base64StringCertificate -PolicyPath $policyPath + $cert.Policy.SecretContentType | Should -Be "application/x-pkcs12" + } + It "ImportWithPrivateKeyFromCollectionWithPolicyFileParameterSet" { + $certName = Get-CertificateName + $certFilePath = "..\Resources\importCertWithPolicy.pfx" + $policyPath = "..\Resources\certPolicy.json" + $certCollection = [System.Security.Cryptography.X509Certificates.X509Certificate2Collection]::new() + $certCollection.Import($certFilePath, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) + + $cert = Import-AzKeyVaultCertificate -VaultName $vaultName -Name $certName -CertificateCollection $certCollection -PolicyPath $policyPath + $cert.Policy.SecretContentType | Should -Be "application/x-pkcs12" + } +} + diff --git a/src/KeyVault/KeyVault.Test/Resources/certPolicy.json b/src/KeyVault/KeyVault.Test/Resources/certPolicy.json new file mode 100644 index 000000000000..88af4f552bc7 --- /dev/null +++ b/src/KeyVault/KeyVault.Test/Resources/certPolicy.json @@ -0,0 +1,40 @@ +{ + "id": "https://myvault.vault.azure.net/certificates/updateCert01/policy", + "key_props": { + "exportable": true, + "kty": "RSA", + "key_size": 2048, + "reuse_key": false + }, + + "secret_props": { + "contentType": "application/x-pkcs12" + }, + + "x509_props": { + "subject": "CN=KeyVaultTest", + "ekus": [], + "key_usage": [], + "validity_months": 297 + }, + + "lifetime_actions": [ + { + "trigger": { + "lifetime_percentage": 80 + }, + "action": { + "action_type": "EmailContacts" + } + } + ], + + "issuer": { + "name": "Unknown" + }, + "attributes": { + "enabled": true, + "created": 1482188947, + "updated": 1482188947 + } +} \ No newline at end of file diff --git a/src/KeyVault/KeyVault.Test/Resources/importCertWithPolicy.pfx b/src/KeyVault/KeyVault.Test/Resources/importCertWithPolicy.pfx new file mode 100644 index 000000000000..f89f48815d0e Binary files /dev/null and b/src/KeyVault/KeyVault.Test/Resources/importCertWithPolicy.pfx differ diff --git a/src/KeyVault/KeyVault/ChangeLog.md b/src/KeyVault/KeyVault/ChangeLog.md index 00d175567d07..d27796c6af38 100644 --- a/src/KeyVault/KeyVault/ChangeLog.md +++ b/src/KeyVault/KeyVault/ChangeLog.md @@ -18,6 +18,7 @@ - Additional information about change #1 --> ## Upcoming Release +* Added parameter `PolicyPath` and `PolicyObject` in `Import-AzKeyVaultCertificate` to support custom policy [#20780] ## Version 4.9.2 * Updated Azure.Core to 1.28.0. diff --git a/src/KeyVault/KeyVault/Commands/Certificate/ImportAzureKeyVaultCertificate.cs b/src/KeyVault/KeyVault/Commands/Certificate/ImportAzureKeyVaultCertificate.cs index fd5db57e2bb0..8015ccf4a18a 100644 --- a/src/KeyVault/KeyVault/Commands/Certificate/ImportAzureKeyVaultCertificate.cs +++ b/src/KeyVault/KeyVault/Commands/Certificate/ImportAzureKeyVaultCertificate.cs @@ -108,6 +108,21 @@ public class ImportAzureKeyVaultCertificate : KeyVaultCmdletBase HelpMessage = "Specifies the password for the certificate and private key base64 encoded string to import.")] public SecureString Password { get; set; } + /// + /// File Path + /// + [Parameter(Mandatory = false, + HelpMessage = "A file path to specify management policy for the certificate that contains JSON encoded policy definition. Mutual-exclusive to PolicyObject.")] + public string PolicyPath { get; set; } + + /// + /// File Path + /// + [Parameter(Mandatory = false, + ValueFromPipeline = true, + HelpMessage = "An in-memory object to specify management policy for the certificate. Mutual-exclusive to PolicyPath.")] + public PSKeyVaultCertificatePolicy PolicyObject { get; set; } + /// /// Certificate Collection /// @@ -131,6 +146,7 @@ public class ImportAzureKeyVaultCertificate : KeyVaultCmdletBase protected override void BeginProcessing() { FilePath = this.TryResolvePath(FilePath); + PolicyPath = this.TryResolvePath(PolicyPath); base.BeginProcessing(); } @@ -143,6 +159,34 @@ private void ValidateParameters() { throw new AzPSArgumentException(string.Format(Resources.FileNotFound, this.FilePath), nameof(FilePath)); } + if (IsPemFile(FilePath)) + { + ContentType = Constants.PemContentType; + } + else + { + ContentType = Constants.Pkcs12ContentType; + } + } + if (this.IsParameterBound(c => c.CertificateCollection)) + { + ContentType = Constants.Pkcs12ContentType; + } + if (this.IsParameterBound(c => c.PolicyPath) && this.IsParameterBound(c => c.PolicyObject)) + { + throw new AzPSArgumentException($"Parameter {nameof(PolicyPath)} conflicts with Parameter {nameof(PolicyObject)}. Only one of these 2 parameters could be imported at once.", nameof(PolicyPath)); + } + if (this.IsParameterBound(c => c.PolicyPath)) + { + if (!File.Exists(PolicyPath)) + { + throw new AzPSArgumentException(string.Format(Resources.FileNotFound, this.PolicyPath), nameof(PolicyPath)); + } + PolicyObject = PSKeyVaultCertificatePolicy.FromJsonFile(PolicyPath); + } + if (PolicyObject != null && PolicyObject.SecretContentType != ContentType) + { + throw new AzPSArgumentException($"User input {ContentType} conflicts with the ContentType stated as {PolicyObject.SecretContentType} in Certificate Policy.", ContentType); } } @@ -162,7 +206,7 @@ public override void ExecuteCmdlet() if (IsPemFile(FilePath)) { byte[] pemBytes = File.ReadAllBytes(FilePath); - certBundle = this.Track2DataClient.ImportCertificate(VaultName, Name, pemBytes, Password, Tag?.ConvertToDictionary(), Constants.PemContentType); + certBundle = this.Track2DataClient.ImportCertificate(VaultName, Name, pemBytes, Password, Tag?.ConvertToDictionary(), Constants.PemContentType, certPolicy: PolicyObject); } else { @@ -179,8 +223,9 @@ public override void ExecuteCmdlet() if (doImport) { + byte[] base64Bytes = userProvidedCertColl.Export(X509ContentType.Pfx, Password?.ConvertToString()); - certBundle = this.Track2DataClient.ImportCertificate(VaultName, Name, base64Bytes, Password, Tag?.ConvertToDictionary()); + certBundle = this.Track2DataClient.ImportCertificate(VaultName, Name, base64Bytes, Password, Tag?.ConvertToDictionary(), certPolicy: PolicyObject); } else { @@ -194,12 +239,11 @@ public override void ExecuteCmdlet() break; case ImportWithPrivateKeyFromCollectionParameterSet: - certBundle = this.DataServiceClient.ImportCertificate(VaultName, Name, CertificateCollection, Tag?.ConvertToDictionary()); - + certBundle = this.Track2DataClient.ImportCertificate(VaultName, Name, CertificateCollection, null, Tag?.ConvertToDictionary(), certPolicy: PolicyObject); break; case ImportWithPrivateKeyFromStringParameterSet: - certBundle = this.Track2DataClient.ImportCertificate(VaultName, Name, CertificateString, Password, Tag?.ConvertToDictionary(), ContentType); + certBundle = this.Track2DataClient.ImportCertificate(VaultName, Name, CertificateString, Password, Tag?.ConvertToDictionary(), ContentType, certPolicy: PolicyObject); break; } diff --git a/src/KeyVault/KeyVault/Models/IKeyVaultDataServiceClient.cs b/src/KeyVault/KeyVault/Models/IKeyVaultDataServiceClient.cs index a0d1584057b4..6e13e202d3c3 100644 --- a/src/KeyVault/KeyVault/Models/IKeyVaultDataServiceClient.cs +++ b/src/KeyVault/KeyVault/Models/IKeyVaultDataServiceClient.cs @@ -164,11 +164,11 @@ public interface IKeyVaultDataServiceClient PSKeyVaultCertificate MergeCertificate(string vaultName, string certName, byte[] certBytes, Dictionary tags); - PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, byte[] certificate, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType); + PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, byte[] certificate, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType, PSKeyVaultCertificatePolicy certPolicy = null); - PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, string base64CertString, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType); + PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, string base64CertString, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType, PSKeyVaultCertificatePolicy certPolicy = null); - PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, X509Certificate2Collection certificateCollection, IDictionary tags, string contentType = Constants.Pkcs12ContentType); + PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, X509Certificate2Collection certificateCollection, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType, PSKeyVaultCertificatePolicy certPolicy = null); PSDeletedKeyVaultCertificate DeleteCertificate(string vaultName, string certName); diff --git a/src/KeyVault/KeyVault/Models/KeyVaultDataServiceClient.cs b/src/KeyVault/KeyVault/Models/KeyVaultDataServiceClient.cs index 11798d35b8c1..77fa0982078c 100644 --- a/src/KeyVault/KeyVault/Models/KeyVaultDataServiceClient.cs +++ b/src/KeyVault/KeyVault/Models/KeyVaultDataServiceClient.cs @@ -817,12 +817,12 @@ public PSKeyVaultCertificate MergeCertificate(string vaultName, string certName, } - public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, byte[] certificate, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType) + public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, byte[] certificate, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType, PSKeyVaultCertificatePolicy certPolicyPath = null) { return ImportCertificate(vaultName, certName, Convert.ToBase64String(certificate), certPassword, tags, contentType); } - public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, string base64CertColl, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType) + public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, string base64CertColl, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType, PSKeyVaultCertificatePolicy certPolicyPath = null) { if (string.IsNullOrEmpty(vaultName)) throw new ArgumentNullException(nameof(vaultName)); @@ -855,7 +855,7 @@ public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName return new PSKeyVaultCertificate(certBundle); } - public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, X509Certificate2Collection certificateCollection, IDictionary tags, string contentType = Constants.Pkcs12ContentType) + public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, X509Certificate2Collection certificateCollection, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType, PSKeyVaultCertificatePolicy certPolicy = null) { if (string.IsNullOrEmpty(vaultName)) throw new ArgumentNullException(nameof(vaultName)); diff --git a/src/KeyVault/KeyVault/Models/PSKeyVaultCertificate.cs b/src/KeyVault/KeyVault/Models/PSKeyVaultCertificate.cs index 467570c99782..8a2f495572d0 100644 --- a/src/KeyVault/KeyVault/Models/PSKeyVaultCertificate.cs +++ b/src/KeyVault/KeyVault/Models/PSKeyVaultCertificate.cs @@ -30,6 +30,7 @@ public class PSKeyVaultCertificate : PSKeyVaultCertificateIdentityItem public string SecretId { get; internal set; } public string Thumbprint { get; set; } + public PSKeyVaultCertificatePolicy Policy { get; set; } public string RecoveryLevel { get; private set; } internal PSKeyVaultCertificate(CertificateBundle certificateBundle, VaultUriHelper vaultUriHelper) @@ -156,6 +157,7 @@ internal PSKeyVaultCertificate(KeyVaultCertificateWithPolicy keyVaultCertificate KeyId = keyVaultCertificate.KeyId?.ToString(); SecretId = keyVaultCertificate.SecretId?.ToString(); + Policy = PSKeyVaultCertificatePolicy.FromTrack2CertificatePolicy(keyVaultCertificate.Policy); if (keyVaultCertificate.Properties != null) { diff --git a/src/KeyVault/KeyVault/Models/PSKeyVaultCertificatePolicy.cs b/src/KeyVault/KeyVault/Models/PSKeyVaultCertificatePolicy.cs index fb1aa76bef37..793d233e8e8b 100644 --- a/src/KeyVault/KeyVault/Models/PSKeyVaultCertificatePolicy.cs +++ b/src/KeyVault/KeyVault/Models/PSKeyVaultCertificatePolicy.cs @@ -12,12 +12,18 @@ // limitations under the License. // ---------------------------------------------------------------------------------- +using Microsoft.Azure.Commands.Common.Exceptions; +using Microsoft.Azure.Commands.KeyVault.Properties; using Microsoft.Azure.KeyVault.Models; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.Json; +using Track2CertificateSDK = Azure.Security.KeyVault.Certificates; namespace Microsoft.Azure.Commands.KeyVault.Models { @@ -31,6 +37,8 @@ public class PSKeyVaultCertificatePolicy public bool? ReuseKeyOnRenewal { get; set; } public string SubjectName { get; set; } public List DnsNames { get; set; } + public List Emails { get; set; } + public List UserPrincipalNames { get; set; } public List KeyUsage { get; set; } public List Ekus { get; set; } public int? ValidityInMonths { get; set; } @@ -274,6 +282,130 @@ internal CertificatePolicy ToCertificatePolicy() return certificatePolicy; } + internal bool HasSubjectAlternativeNames() + { + if ((DnsNames != null && DnsNames.Any()) + || (Emails != null && Emails.Any()) + || (UserPrincipalNames != null && UserPrincipalNames.Any())) + { + return true; + } + else + return false; + } + + internal Track2CertificateSDK.SubjectAlternativeNames SetSubjectAlternativeNames(Track2CertificateSDK.SubjectAlternativeNames SubjectAlternativeNames) + { + if (DnsNames != null && DnsNames.Any()) + foreach (var dnsName in DnsNames) + SubjectAlternativeNames.DnsNames.Add(dnsName); + if (Emails != null && Emails.Any()) + foreach (var email in Emails) + SubjectAlternativeNames.Emails.Add(email); + if (UserPrincipalNames != null && UserPrincipalNames.Any()) + foreach (var principalName in UserPrincipalNames) + SubjectAlternativeNames.UserPrincipalNames.Add(principalName); + return SubjectAlternativeNames; + } + + internal Track2CertificateSDK.CertificatePolicy ToTrack2CertificatePolicy() + { + Track2CertificateSDK.CertificatePolicy certificatePolicy; + if (!string.IsNullOrWhiteSpace(IssuerName) || !string.IsNullOrWhiteSpace(SubjectName) || HasSubjectAlternativeNames()) + { + if (!string.IsNullOrWhiteSpace(SubjectName) && HasSubjectAlternativeNames()) + { + Track2CertificateSDK.SubjectAlternativeNames subjectAlternativeNames = new Track2CertificateSDK.SubjectAlternativeNames(); + subjectAlternativeNames = SetSubjectAlternativeNames(subjectAlternativeNames); + certificatePolicy = new Track2CertificateSDK.CertificatePolicy(IssuerName, SubjectName, subjectAlternativeNames); + + } + else if (!string.IsNullOrWhiteSpace(SubjectName)) + { + certificatePolicy = new Track2CertificateSDK.CertificatePolicy(IssuerName, SubjectName); + } + else if (HasSubjectAlternativeNames()) + { + Track2CertificateSDK.SubjectAlternativeNames subjectAlternativeNames = new Track2CertificateSDK.SubjectAlternativeNames(); + subjectAlternativeNames = SetSubjectAlternativeNames(subjectAlternativeNames); + certificatePolicy = new Track2CertificateSDK.CertificatePolicy(IssuerName, subjectAlternativeNames); + } + else + certificatePolicy = new Track2CertificateSDK.CertificatePolicy(IssuerName, SubjectName); + } + else + { + certificatePolicy = new Track2CertificateSDK.CertificatePolicy(); + } + certificatePolicy.ContentType = SecretContentType; + if ( !string.IsNullOrEmpty(Kty) ) + certificatePolicy.KeyType = Kty; + certificatePolicy.KeySize = KeySize; + if (!string.IsNullOrEmpty(Curve)) + certificatePolicy.KeyCurveName = Curve; + certificatePolicy.ReuseKey = ReuseKeyOnRenewal; + certificatePolicy.Exportable = Exportable; + certificatePolicy.CertificateTransparency = CertificateTransparency; + if (!string.IsNullOrWhiteSpace(CertificateType)) + certificatePolicy.CertificateType = CertificateType; + certificatePolicy.Enabled = Enabled; + certificatePolicy.ValidityInMonths = ValidityInMonths; + + if (RenewAtNumberOfDaysBeforeExpiry.HasValue || + RenewAtPercentageLifetime.HasValue || + EmailAtNumberOfDaysBeforeExpiry.HasValue || + EmailAtPercentageLifetime.HasValue) + { + if ((RenewAtNumberOfDaysBeforeExpiry.HasValue ? 1 : 0) + + (RenewAtPercentageLifetime.HasValue ? 1 : 0) + + (EmailAtNumberOfDaysBeforeExpiry.HasValue ? 1 : 0) + + (EmailAtPercentageLifetime.HasValue ? 1 : 0) > 1) + { + throw new ArgumentException("Only one of the values for RenewAtNumberOfDaysBeforeExpiry, RenewAtPercentageLifetime, EmailAtNumberOfDaysBeforeExpiry, EmailAtPercentageLifetime can be set."); + } + + if (RenewAtNumberOfDaysBeforeExpiry.HasValue) + { + certificatePolicy.LifetimeActions.Add( + new Track2CertificateSDK.LifetimeAction(Track2CertificateSDK.CertificatePolicyAction.AutoRenew) + { + DaysBeforeExpiry = RenewAtNumberOfDaysBeforeExpiry + } + ); + } + + if (RenewAtPercentageLifetime.HasValue) + { + certificatePolicy.LifetimeActions.Add( + new Track2CertificateSDK.LifetimeAction(Track2CertificateSDK.CertificatePolicyAction.AutoRenew) + { + LifetimePercentage = RenewAtPercentageLifetime + } + ); + } + if (EmailAtNumberOfDaysBeforeExpiry.HasValue) + { + certificatePolicy.LifetimeActions.Add( + new Track2CertificateSDK.LifetimeAction(Track2CertificateSDK.CertificatePolicyAction.EmailContacts) + { + DaysBeforeExpiry = EmailAtNumberOfDaysBeforeExpiry + } + ); + } + + if (EmailAtPercentageLifetime.HasValue) + { + certificatePolicy.LifetimeActions.Add( + new Track2CertificateSDK.LifetimeAction(Track2CertificateSDK.CertificatePolicyAction.EmailContacts) + { + LifetimePercentage = EmailAtPercentageLifetime + } + ); + } + } + + return certificatePolicy; + } internal static PSKeyVaultCertificatePolicy FromCertificatePolicy(CertificatePolicy certificatePolicy) { @@ -304,6 +436,258 @@ internal static PSKeyVaultCertificatePolicy FromCertificatePolicy(CertificatePol }; } + internal static PSKeyVaultCertificatePolicy FromTrack2CertificatePolicy(Track2CertificateSDK.CertificatePolicy certificatePolicy) + { + return new PSKeyVaultCertificatePolicy + { + SecretContentType = certificatePolicy.ContentType?.ToString(), + Kty = certificatePolicy.KeyType?.ToString(), + KeySize = certificatePolicy.KeySize, + Curve = certificatePolicy.KeyCurveName?.ToString(), + Exportable = certificatePolicy.Exportable, + ReuseKeyOnRenewal = certificatePolicy.ReuseKey, + SubjectName = certificatePolicy.Subject, + DnsNames = certificatePolicy.SubjectAlternativeNames?.DnsNames?.ToList(), + Emails = certificatePolicy.SubjectAlternativeNames?.Emails?.ToList(), + UserPrincipalNames = certificatePolicy.SubjectAlternativeNames?.UserPrincipalNames?.ToList(), + KeyUsage = certificatePolicy.KeyUsage == null ? null : certificatePolicy.KeyUsage == null ? null : certificatePolicy.KeyUsage.Select(keyUsage => keyUsage.ToString()).ToList(), + Ekus = certificatePolicy.EnhancedKeyUsage == null ? null : new List(certificatePolicy.EnhancedKeyUsage), + ValidityInMonths = certificatePolicy.ValidityInMonths, + CertificateTransparency = certificatePolicy.CertificateTransparency, + IssuerName = certificatePolicy.IssuerName, + CertificateType = certificatePolicy.CertificateType, + RenewAtNumberOfDaysBeforeExpiry = certificatePolicy.LifetimeActions == null ? null : FindIntValueForAutoRenewAction(certificatePolicy.LifetimeActions), + RenewAtPercentageLifetime = certificatePolicy.LifetimeActions == null ? null : FindIntValueForAutoRenewAction(certificatePolicy.LifetimeActions), + EmailAtNumberOfDaysBeforeExpiry = certificatePolicy.LifetimeActions == null ? null : FindIntValueForEmailAction(certificatePolicy.LifetimeActions), + EmailAtPercentageLifetime = certificatePolicy.LifetimeActions == null ? null : FindIntValueForEmailAction(certificatePolicy.LifetimeActions), + Enabled = certificatePolicy.Enabled, + Created = certificatePolicy.CreatedOn.HasValue ? certificatePolicy.CreatedOn.Value.DateTime : (DateTime?)null, + Updated = certificatePolicy.UpdatedOn.HasValue ? certificatePolicy.UpdatedOn.Value.DateTime : (DateTime?)null, + }; + } + private void ReadKeyProperties(JsonElement json) + { + foreach (JsonProperty item in json.EnumerateObject()) + { + switch (item.Name) + { + case "kty": + Kty = item.Value.GetString(); + break; + case "reuse_key": + ReuseKeyOnRenewal = item.Value.GetBoolean(); + break; + case "exportable": + Exportable = item.Value.GetBoolean(); + break; + case "crv": + Curve = item.Value.GetString(); + break; + case "key_size": + KeySize = item.Value.GetInt32(); + break; + } + } + } + private void ReadSecretProperties(JsonElement json) + { + if (json.TryGetProperty("contentType", out var value)) + { + SecretContentType = value.GetString(); + } + } + + private void ReadSubjectAlternativeNames(JsonProperty json) + { + foreach (JsonProperty item in json.Value.EnumerateObject()) + { + switch (item.Name) + { + case "dns_names": + DnsNames = new List(); + foreach (JsonElement item2 in item.Value.EnumerateArray()) + { + DnsNames.Add(item2.GetString()); + } + break; + case "emails": + Emails = new List(); + foreach (JsonElement item2 in item.Value.EnumerateArray()) + { + Emails.Add(item2.GetString()); + } + break; + case "upns": + UserPrincipalNames = new List(); + foreach (JsonElement item2 in item.Value.EnumerateArray()) + { + UserPrincipalNames.Add(item2.GetString()); + } + break; + } + } + } + + private void ReadX509CertificateProperties(JsonElement json) + { + foreach (JsonProperty item in json.EnumerateObject()) + { + switch (item.Name) + { + case "subject": + SubjectName = item.Value.GetString(); + break; + case "sans": + var SubjectAlternativeNames = new Track2CertificateSDK.SubjectAlternativeNames(); + ReadSubjectAlternativeNames(item); + // SubjectAlternativeNames .ReadProperties(item.Value); + break; + case "key_usage": + foreach (JsonElement item2 in item.Value.EnumerateArray()) + { + KeyUsage.Add(item2.GetString()); + } + + break; + case "ekus": + foreach (JsonElement item3 in item.Value.EnumerateArray()) + { + Ekus.Add(item3.GetString()); + } + + break; + case "validity_months": + ValidityInMonths = item.Value.GetInt32(); + break; + } + } + } + + private void ReadIssuerProperties(JsonElement json) + { + foreach (JsonProperty item in json.EnumerateObject()) + { + switch (item.Name) + { + case "cert_transparency": + CertificateTransparency = item.Value.GetBoolean(); + break; + + case "cty": + CertificateType = item.Value.GetString(); + break; + + case "name": + IssuerName = item.Value.GetString(); + break; + } + } + } + + private void ReadAttributesProperties(JsonElement json) + { + foreach (JsonProperty item in json.EnumerateObject()) + { + switch (item.Name) + { + case "enabled": + Enabled = item.Value.GetBoolean(); + break; + case "created": + Created = DateTimeOffset.FromUnixTimeSeconds(item.Value.GetInt64()).DateTime; + break; + case "updated": + Updated = DateTimeOffset.FromUnixTimeSeconds(item.Value.GetInt64()).DateTime; + break; + } + } + } + internal static PSKeyVaultCertificatePolicy FromJsonFile(string filePath) + { + PSKeyVaultCertificatePolicy policy = new PSKeyVaultCertificatePolicy(); + JsonElement policyJson; + if (".json".Equals(Path.GetExtension(filePath), StringComparison.OrdinalIgnoreCase)) + { + using (StreamReader r = new StreamReader(filePath)) + { + string jsonContent = r.ReadToEnd(); + policyJson = JsonDocument.Parse(jsonContent).RootElement; + } + } + else + { + throw new AzPSArgumentException(string.Format(Resources.UnsupportedFileFormat, filePath), nameof(filePath)); + } + foreach (JsonProperty item in policyJson.EnumerateObject()) + { + switch (item.Name) + { + case "key_props": + policy.ReadKeyProperties(item.Value); + break; + case "secret_props": + policy.ReadSecretProperties(item.Value); + break; + case "x509_props": + policy.ReadX509CertificateProperties(item.Value); + break; + case "issuer": + policy.ReadIssuerProperties(item.Value); + break; + case "attributes": + policy.ReadAttributesProperties(item.Value); + break; + + case "lifetime_actions": + string actionType = null; + string triggerType = null; + int? triggerValue = null; + + foreach (JsonElement item2 in item.Value.EnumerateArray()) + { + if (item.Value.EnumerateArray().Count() > 1) + throw new AzPSArgumentException(string.Format("Json file property {0} exceed expected number 1.", item.Name), nameof(item.Name)); + foreach (JsonProperty item3 in item2.EnumerateObject()) + { + if (item3.Name == "trigger") + { + foreach (JsonProperty item4 in item3.Value.EnumerateObject()) + { + triggerType = item4.Name; + triggerValue = item4.Value.GetInt32(); + } + } + else if (item3.Name == "action") + { + foreach (JsonProperty item4 in item3.Value.EnumerateObject()) + { + if (item4.Name == "action_type") + actionType = item4.Value.GetString(); + } + } + } + } + + if (actionType == ActionType.AutoRenew.ToString()) + { + if (triggerType == "days_before_expiry") + policy.RenewAtNumberOfDaysBeforeExpiry = triggerValue; + else if (triggerType == "lifetime_percentage") + policy.RenewAtPercentageLifetime = triggerValue; + } + else if (actionType == ActionType.EmailContacts.ToString()) + { + if (triggerType == "days_before_expiry") + policy.EmailAtNumberOfDaysBeforeExpiry = triggerValue; + else if (triggerType == "lifetime_percentage") + policy.EmailAtPercentageLifetime = triggerValue; + } + break; + } + } + return policy; + } + private static int? FindIntValueForAutoRenewAction(IEnumerable lifetimeActions, Func intValueGetter) { var lifetimeAction = lifetimeActions.FirstOrDefault(x => x.Action.ActionType.HasValue && 0 == string.Compare(x.Action.ActionType.Value.ToString(), ActionType.AutoRenew.ToString(), true) && intValueGetter(x.Trigger).HasValue); @@ -316,6 +700,14 @@ internal static PSKeyVaultCertificatePolicy FromCertificatePolicy(CertificatePol return intValueGetter(lifetimeAction.Trigger); } + private static int? FindIntValueForAutoRenewAction(IList lifetimeActions) + { + var lifetimeAction = + lifetimeActions.FirstOrDefault(x => string.IsNullOrEmpty(x.Action.ToString()) && 0 == string.Compare(x.Action.ToString(), Track2CertificateSDK.CertificatePolicyAction.AutoRenew.ToString(), true) + && (x.DaysBeforeExpiry.HasValue || x.LifetimePercentage.HasValue)); + return lifetimeAction == null ? null : (lifetimeAction.DaysBeforeExpiry ?? lifetimeAction.LifetimePercentage); + } + private static int? FindIntValueForEmailAction(IEnumerable lifetimeActions, Func intValueGetter) { var lifetimeAction = lifetimeActions.FirstOrDefault(x => x.Action.ActionType.HasValue && 0 == string.Compare(x.Action.ActionType.Value.ToString(), ActionType.EmailContacts.ToString(), true) && intValueGetter(x.Trigger).HasValue); @@ -328,6 +720,14 @@ internal static PSKeyVaultCertificatePolicy FromCertificatePolicy(CertificatePol return intValueGetter(lifetimeAction.Trigger); } + private static int? FindIntValueForEmailAction(IList lifetimeActions) + { + var lifetimeAction = + lifetimeActions.FirstOrDefault(x => !string.IsNullOrEmpty(x.Action.ToString()) && 0 == string.Compare(x.Action.ToString(), Track2CertificateSDK.CertificatePolicyAction.EmailContacts.ToString(), true) + && (x.DaysBeforeExpiry.HasValue || x.LifetimePercentage.HasValue)); + return lifetimeAction == null ? null : (lifetimeAction.DaysBeforeExpiry ?? lifetimeAction.LifetimePercentage); + } + private void ValidateInternal( IList dnsNames, IList ekus, @@ -544,5 +944,19 @@ private void ValidateSubjectName(string subjectName) throw new ArgumentException("The subject name provided is not a valid X500 name.", e); } } + + public override string ToString() + { + if (this == null) return string.Empty; + + var sb = new StringBuilder(); + sb.AppendLine(); + sb.AppendFormat("{0, -15}: {1}{2}", "Secret Content Type", SecretContentType, Environment.NewLine); + sb.AppendFormat("{0, -15}: {1}{2}", "Issuer Name", IssuerName, Environment.NewLine); + sb.AppendFormat("{0, -15}: {1}{2}", "Created On", Created, Environment.NewLine); + sb.AppendFormat("{0, -15}: {1}{2}", "Updated On", Updated, Environment.NewLine); + sb.AppendLine("..."); + return sb.ToString(); + } } } diff --git a/src/KeyVault/KeyVault/Track2Models/Track2KeyVaultDataServiceClient.cs b/src/KeyVault/KeyVault/Track2Models/Track2KeyVaultDataServiceClient.cs index 1c275a998d85..cb4fe26e63d2 100644 --- a/src/KeyVault/KeyVault/Track2Models/Track2KeyVaultDataServiceClient.cs +++ b/src/KeyVault/KeyVault/Track2Models/Track2KeyVaultDataServiceClient.cs @@ -239,19 +239,20 @@ public IEnumerable GetDeletedCertifica throw new NotImplementedException(); } - public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, string certificate, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType) + public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, string certificate, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType, PSKeyVaultCertificatePolicy certPolicy = null) { - return VaultClient.ImportCertificate(vaultName, certName, Convert.FromBase64String(certificate), certPassword, tags, contentType); + return VaultClient.ImportCertificate(vaultName, certName, Convert.FromBase64String(certificate), certPassword, tags, contentType, certPolicy); } - public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, byte[] certificate, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType) + public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, byte[] certificate, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType, PSKeyVaultCertificatePolicy certPolicy = null) { - return VaultClient.ImportCertificate(vaultName, certName, certificate, certPassword, tags, contentType); + return VaultClient.ImportCertificate(vaultName, certName, certificate, certPassword, tags, contentType, certPolicy); } - public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, X509Certificate2Collection certificateCollection, IDictionary tags, string contentType = Constants.Pkcs12ContentType) + public PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, X509Certificate2Collection certificateCollection, SecureString certPassword, IDictionary tags, string contentType = Constants.Pkcs12ContentType, PSKeyVaultCertificatePolicy certPolicy = null) { - throw new NotImplementedException(); + // Export contentType ref: https://github.com/Azure/azure-sdk-for-net/blob/376b04164356dc9821923b75f2223163a2701669/sdk/keyvault/Microsoft.Azure.KeyVault/src/Customized/KeyVaultClientExtensions.cs#L605 + return VaultClient.ImportCertificate(vaultName, certName, certificateCollection.Export(X509ContentType.Pfx), certPassword, tags, contentType, certPolicy); } public PSKeyVaultCertificate MergeCertificate(string vaultName, string certName, X509Certificate2Collection certs, IDictionary tags) diff --git a/src/KeyVault/KeyVault/Track2Models/Track2VaultClient.cs b/src/KeyVault/KeyVault/Track2Models/Track2VaultClient.cs index 92f206f5f2fe..c9eae3ec046a 100644 --- a/src/KeyVault/KeyVault/Track2Models/Track2VaultClient.cs +++ b/src/KeyVault/KeyVault/Track2Models/Track2VaultClient.cs @@ -3,12 +3,14 @@ using Azure.Security.KeyVault.Keys.Cryptography; using Microsoft.Azure.Commands.Common.Authentication.Abstractions; +using Microsoft.Azure.Commands.Common.Exceptions; using Microsoft.Azure.Commands.KeyVault.Models; using Microsoft.WindowsAzure.Commands.Utilities.Common; using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Security; namespace Microsoft.Azure.Commands.KeyVault.Track2Models @@ -211,7 +213,7 @@ private PSKeyRotationPolicy SetKeyRotationPolicy(KeyClient client, string vaultN #endregion #region Certificate actions - internal PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, byte[] certificate, SecureString password, IDictionary tags, string contentType = Constants.Pkcs12ContentType) + internal PSKeyVaultCertificate ImportCertificate(string vaultName, string certName, byte[] certificate, SecureString password, IDictionary tags, string contentType = Constants.Pkcs12ContentType, PSKeyVaultCertificatePolicy certPolicy = null) { if (string.IsNullOrEmpty(vaultName)) throw new ArgumentNullException(nameof(vaultName)); @@ -221,17 +223,18 @@ internal PSKeyVaultCertificate ImportCertificate(string vaultName, string certNa throw new ArgumentNullException(nameof(certificate)); var certClient = CreateCertificateClient(vaultName); - return ImportCertificate(certClient, certName, certificate, password, tags, contentType); + return ImportCertificate(certClient, certName, certificate, password, tags, contentType, certPolicy); } - private PSKeyVaultCertificate ImportCertificate(CertificateClient certClient, string certName, byte[] certificate, SecureString password, IDictionary tags, string contentType = Constants.Pkcs12ContentType) + private PSKeyVaultCertificate ImportCertificate(CertificateClient certClient, string certName, byte[] certificate, SecureString password, IDictionary tags, string contentType = Constants.Pkcs12ContentType, PSKeyVaultCertificatePolicy certPolicy = null) { + CertificatePolicy certificatePolicy = certPolicy?.ToTrack2CertificatePolicy() ?? new CertificatePolicy() + { + ContentType = contentType + }; var options = new ImportCertificateOptions(certName, certificate) { - Policy = new CertificatePolicy() - { - ContentType = contentType - }, + Policy = certificatePolicy, Password = password?.ConvertToString() }; tags?.ForEach((entry) => diff --git a/src/KeyVault/KeyVault/help/Import-AzKeyVaultCertificate.md b/src/KeyVault/KeyVault/help/Import-AzKeyVaultCertificate.md index dc32a2f63008..db1d990f609a 100644 --- a/src/KeyVault/KeyVault/help/Import-AzKeyVaultCertificate.md +++ b/src/KeyVault/KeyVault/help/Import-AzKeyVaultCertificate.md @@ -16,22 +16,23 @@ Imports a certificate to a key vault. ### ImportCertificateFromFile (Default) ``` Import-AzKeyVaultCertificate [-VaultName] [-Name] -FilePath - [-Password ] [-Tag ] [-DefaultProfile ] [-WhatIf] [-Confirm] - [] + [-Password ] [-PolicyPath ] [-PolicyObject ] + [-Tag ] [-DefaultProfile ] [-WhatIf] [-Confirm] [] ``` ### ImportWithPrivateKeyFromString ``` Import-AzKeyVaultCertificate [-VaultName] [-Name] -CertificateString - [-ContentType ] [-Password ] [-Tag ] - [-DefaultProfile ] [-WhatIf] [-Confirm] [] + [-ContentType ] [-Password ] [-PolicyPath ] + [-PolicyObject ] [-Tag ] [-DefaultProfile ] + [-WhatIf] [-Confirm] [] ``` ### ImportWithPrivateKeyFromCollection ``` -Import-AzKeyVaultCertificate [-VaultName] [-Name] - [-CertificateCollection] [-Tag ] - [-DefaultProfile ] [-WhatIf] [-Confirm] [] +Import-AzKeyVaultCertificate [-VaultName] [-Name] [-PolicyPath ] + [-PolicyObject ] [-CertificateCollection] + [-Tag ] [-DefaultProfile ] [-WhatIf] [-Confirm] [] ``` ## DESCRIPTION @@ -84,7 +85,6 @@ The second command imports the certificate named ImportCert01 into the CosotosoK $Password = ConvertTo-SecureString -String "123" -AsPlainText -Force $Base64String = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("import.pfx")) Import-AzKeyVaultCertificate -VaultName "ContosoKV01" -Name "ImportCert01" -CertificateString $Base64String -Password $Password - ``` ```output @@ -119,6 +119,59 @@ stores it in the $Password variable. The second command reads a certificate as a Base64 encoded representation. The third command imports the certificate named ImportCert01 into the CosotosoKV01 key vault. +### Example 3: Import a key vault certificate with PolicyFile +```powershell +$Password = ConvertTo-SecureString -String "123" -AsPlainText -Force +Import-AzKeyVaultCertificate -VaultName "ContosoKV01" -Name "ImportCert01" -FilePath "C:\Users\contosoUser\Desktop\import.pfx" -Password $Password -PolicyPath "C:\Users\contosoUser\Desktop\policy.json" +``` + +```output +Name : importCert01 +Certificate : [Subject] + CN=contoso.com + + [Issuer] + CN=contoso.com + + [Serial Number] + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + + [Not Before] + 2/8/2016 3:11:45 PM + + [Not After] + 8/8/2016 4:21:45 PM + + [Thumbprint] + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + +KeyId : https://ContosoKV01.vault.azure.net/keys/ImportCert01/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +SecretId : https://ContosoKV01.vault.azure.net/secrets/ImportCert01/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +Thumbprint : XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +Policy : + Secret Content Type: application/x-pkcs12 + Issuer Name : Unknown + Created On : 3/22/2023 6:00:52 AM + Updated On : 4/27/2023 9:52:53 AM + ... +RecoveryLevel : Recoverable+Purgeable +Enabled : True +Expires : 6/9/2023 6:20:26 AM +NotBefore : 3/11/2023 6:20:26 AM +Created : 4/24/2023 9:05:51 AM +Updated : 4/24/2023 9:05:51 AM +Tags : {} +VaultName : ContosoKV01 +Name : ImportCert01 +Version : XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +Id : https://ContosoKV01.vault.azure.net/certificates/ImportCert01/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +``` + +The first command uses the ConvertTo-SecureString cmdlet to create a secure password, and then +stores it in the $Password variable. +The second command imports the certificate named ImportCert01 into the CosotosoKV01 key vault with +a policy defined by file. + ## PARAMETERS ### -CertificateCollection @@ -227,6 +280,36 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -PolicyObject +An in-memory object to specify management policy for the certificate. Mutual-exclusive to PolicyPath. + +```yaml +Type: Microsoft.Azure.Commands.KeyVault.Models.PSKeyVaultCertificatePolicy +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -PolicyPath +A file path to specify management policy for the certificate that contains JSON encoded policy definition. Mutual-exclusive to PolicyObject. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -Tag Key-value pairs in the form of a hash table. For example: @{key0="value0";key1=$null;key2="value2"}