diff --git a/AzureKeyVault/AkvProperties.cs b/AzureKeyVault/AkvProperties.cs index 4310739..42e7a66 100644 --- a/AzureKeyVault/AkvProperties.cs +++ b/AzureKeyVault/AkvProperties.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System.Collections.Generic; diff --git a/AzureKeyVault/AzureClient.cs b/AzureKeyVault/AzureClient.cs index 8bd0a7f..e6751e6 100644 --- a/AzureKeyVault/AzureClient.cs +++ b/AzureKeyVault/AzureClient.cs @@ -1,14 +1,14 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; using System.Linq; -using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Azure; using Azure.Core; @@ -17,11 +17,13 @@ using Azure.ResourceManager.KeyVault; using Azure.ResourceManager.KeyVault.Models; using Azure.ResourceManager.Resources; +using Azure.ResourceManager.Resources.Models; using Azure.Security.KeyVault.Certificates; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; namespace Keyfactor.Extensions.Orchestrator.AzureKeyVault { @@ -194,7 +196,7 @@ public virtual async Task CreateVault() } } - public virtual async Task ImportCertificateAsync(string certName, string contents, string pfxPassword) + public virtual async Task ImportCertificateAsync(string certName, string contents, string pfxPassword, string tags = null) { try { @@ -206,22 +208,37 @@ public virtual async Task ImportCertificateAsync( RecoverDeletedCertificateOperation recovery = await CertClient.StartRecoverDeletedCertificateAsync(certName); recovery.WaitForCompletion(); } - logger.LogTrace("begin creating x509 certificate from contents."); - var bytes = Convert.FromBase64String(contents); - var x509Collection = new X509Certificate2Collection();//(bytes, pfxPassword, X509KeyStorageFlags.Exportable); + logger.LogTrace($"converting to pkcs12 without password for importing to keyvault"); + + var p12bytes = Helpers.ConvertPfxToPasswordlessPkcs12(contents, pfxPassword); + + logger.LogTrace($"got a byte array with length {p12bytes.Length}"); + + logger.LogTrace($"calling ImportCertificateAsync on the KeyVault certificate client to import certificate {certName}"); - x509Collection.Import(bytes, pfxPassword, X509KeyStorageFlags.Exportable); + var tagDict = new Dictionary(); - var certWithKey = x509Collection.Export(X509ContentType.Pkcs12); + if (!string.IsNullOrEmpty(tags)) + { + if (!tags.IsValidJson()) + { + logger.LogError($"the entry parameter provided for Certificate Tags: \" {tags} \", does not seem to be valid JSON."); + throw new Exception($"the string \" {tags} \" is not a valid json string. Please enter a valid json string for CertificateTags in the entry parameter or leave empty for no tags to be applied."); + } + logger.LogTrace($"converting the json value provided for tags into a Dictionary"); + tagDict = JsonConvert.DeserializeObject>(tags); + logger.LogTrace($"{tagDict.Count} tag(s) will be associated with the certificate in Azure KeyVault"); + } + var options = new ImportCertificateOptions(certName, p12bytes); - logger.LogTrace($"importing created x509 certificate named {1}", certName); - logger.LogTrace($"There are {x509Collection.Count} certificates in the chain."); - var cert = await CertClient.ImportCertificateAsync(new ImportCertificateOptions(certName, certWithKey)); + foreach (var tag in tagDict.Keys) + { + options.Tags.Add(tag, tagDict[tag]); + } - // var fullCert = _secretClient.GetSecret(certName); - // The certificate must be retrieved as a secret from AKV in order to have the full chain included. + var cert = await CertClient.ImportCertificateAsync(options); return cert; } @@ -278,8 +295,9 @@ public virtual async Task> GetCertificatesAsyn var fullInventoryList = new List(); var failedCount = 0; Exception innerException = null; - - await foreach (var cert in inventory) { + + await foreach (var cert in inventory) + { logger.LogTrace($"adding cert with ID: {cert.Id} to the list."); fullInventoryList.Add(cert); // convert to list from pages } @@ -300,23 +318,26 @@ public virtual async Task> GetCertificatesAsyn PrivateKeyEntry = true, ItemStatus = OrchestratorInventoryItemStatus.Unknown, UseChainLevel = true, - Certificates = new List() { Convert.ToBase64String(cert.Value.Cer) } + Certificates = new List() { Convert.ToBase64String(cert.Value.Cer) }, + Parameters = cert.Value.Properties.Tags as Dictionary }); } catch (Exception ex) { failedCount++; innerException = ex; - logger.LogError($"Failed to retreive details for certificate {certificate.Name}. Exception: {ex.Message}"); + logger.LogError($"Failed to retreive details for certificate {certificate.Name}. Exception: {ex.Message}"); // continuing with inventory instead of throwing, in case there's an issue with a single certificate } } - if (failedCount == fullInventoryList.Count()) { + if (failedCount == fullInventoryList.Count() && failedCount > 0) + { throw new Exception("Unable to retreive details for certificates.", innerException); } - if (failedCount > 0) { + if (failedCount > 0) + { logger.LogWarning($"{failedCount} of {fullInventoryList.Count()} certificates were not able to be retreieved. Please review the errors."); } diff --git a/AzureKeyVault/AzureKeyVault.csproj b/AzureKeyVault/AzureKeyVault.csproj index be88991..53cf1e0 100644 --- a/AzureKeyVault/AzureKeyVault.csproj +++ b/AzureKeyVault/AzureKeyVault.csproj @@ -18,23 +18,24 @@ - - + + - - + + + - - - + + + diff --git a/AzureKeyVault/Constants.cs b/AzureKeyVault/Constants.cs index c142ca5..8973edf 100644 --- a/AzureKeyVault/Constants.cs +++ b/AzureKeyVault/Constants.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. namespace Keyfactor.Extensions.Orchestrator.AzureKeyVault { diff --git a/AzureKeyVault/Helpers.cs b/AzureKeyVault/Helpers.cs new file mode 100644 index 0000000..fd934be --- /dev/null +++ b/AzureKeyVault/Helpers.cs @@ -0,0 +1,89 @@ + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using System; +using System.IO; +using System.Text.Json.Nodes; + +namespace Keyfactor.Extensions.Orchestrator.AzureKeyVault +{ + public static class Helpers + { + public static bool IsValidJson(this string jsonString) + { + try + { + var unescapedJSON = System.Text.RegularExpressions.Regex.Unescape(jsonString); + var tmpObj = JsonValue.Parse(unescapedJSON); + } + catch (FormatException fex) + { + //Invalid json format + return false; + } + catch (Exception ex) //some other exception + { + return false; + } + return true; + } + public static byte[] ConvertPfxToPasswordlessPkcs12(string base64Pfx, string pfxPassword) + { + // Decode the Base64-encoded PFX data + byte[] pfxBytes = Convert.FromBase64String(base64Pfx); + using (var inputStream = new MemoryStream(pfxBytes)) + { + var builder = new Pkcs12StoreBuilder(); + builder.SetUseDerEncoding(true); + var store = builder.Build(); + store.Load(inputStream, pfxPassword.ToCharArray()); + + string alias = null; + foreach (string a in store.Aliases) + { + if (store.IsKeyEntry(a)) + { + alias = a; + break; + } + } + + using (var outputStream = new MemoryStream()) + { + var newStore = builder.Build(); + + if (alias != null) + { + // Extract private key and certificate chain if available + var keyEntry = store.GetKey(alias); + var chain = store.GetCertificateChain(alias); + newStore.SetKeyEntry("converted-key", keyEntry, chain); + } + else + { + // If no private key, include just the certificate chain + foreach (string certAlias in store.Aliases) + { + if (store.IsCertificateEntry(certAlias)) + { + var cert = store.GetCertificate(certAlias); + newStore.SetCertificateEntry(certAlias, cert); + } + } + } + + // Save the new PKCS#12 store without a password + newStore.Save(outputStream, null, new SecureRandom()); + return outputStream.ToArray(); + } + } + } + } +} diff --git a/AzureKeyVault/JobAttribute.cs b/AzureKeyVault/JobAttribute.cs index 4b52863..69c8f06 100644 --- a/AzureKeyVault/JobAttribute.cs +++ b/AzureKeyVault/JobAttribute.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; diff --git a/AzureKeyVault/Jobs/AzureKeyVaultJob.cs b/AzureKeyVault/Jobs/AzureKeyVaultJob.cs index 63c9f41..48d243f 100644 --- a/AzureKeyVault/Jobs/AzureKeyVaultJob.cs +++ b/AzureKeyVault/Jobs/AzureKeyVaultJob.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; diff --git a/AzureKeyVault/Jobs/Discovery.cs b/AzureKeyVault/Jobs/Discovery.cs index 1dc4233..6d0fdbe 100644 --- a/AzureKeyVault/Jobs/Discovery.cs +++ b/AzureKeyVault/Jobs/Discovery.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; diff --git a/AzureKeyVault/Jobs/Inventory.cs b/AzureKeyVault/Jobs/Inventory.cs index d0783a2..da55311 100644 --- a/AzureKeyVault/Jobs/Inventory.cs +++ b/AzureKeyVault/Jobs/Inventory.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; diff --git a/AzureKeyVault/Jobs/Management.cs b/AzureKeyVault/Jobs/Management.cs index 022ad6e..5fdd3ce 100644 --- a/AzureKeyVault/Jobs/Management.cs +++ b/AzureKeyVault/Jobs/Management.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Linq; @@ -38,6 +39,8 @@ public JobResult ProcessJob(ManagementJobConfiguration config) FailureMessage = "Invalid Management Operation" }; + var tagsJSON = config.JobProperties["CertificateTags"].ToString(); + switch (config.OperationType) { case CertStoreOperationType.Create: @@ -46,11 +49,12 @@ public JobResult ProcessJob(ManagementJobConfiguration config) break; case CertStoreOperationType.Add: logger.LogDebug($"Begin Management > Add..."); - complete = PerformAddition(config.JobCertificate.Alias, config.JobCertificate.PrivateKeyPassword, config.JobCertificate.Contents, config.JobHistoryId, config.Overwrite); + + complete = PerformAddition(config.JobCertificate.Alias, config.JobCertificate.PrivateKeyPassword, config.JobCertificate.Contents, tagsJSON, config.JobHistoryId, config.Overwrite); break; case CertStoreOperationType.Remove: logger.LogDebug($"Begin Management > Remove..."); - complete = PerformRemoval(config.JobCertificate.Alias, config.JobHistoryId); + complete = PerformRemoval(config.JobCertificate.Alias, tagsJSON, config.JobHistoryId); break; } @@ -88,7 +92,7 @@ protected async Task PerformCreateVault(long jobHistoryId) #endregion #region Add - protected virtual JobResult PerformAddition(string alias, string pfxPassword, string entryContents, long jobHistoryId, bool overwrite) + protected virtual JobResult PerformAddition(string alias, string pfxPassword, string entryContents, string tagsJSON, long jobHistoryId, bool overwrite) { var complete = new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = jobHistoryId }; @@ -116,7 +120,8 @@ protected virtual JobResult PerformAddition(string alias, string pfxPassword, st return complete; } } - var cert = AzClient.ImportCertificateAsync(alias, entryContents, pfxPassword).Result; + + var cert = AzClient.ImportCertificateAsync(alias, entryContents, pfxPassword, tagsJSON).Result; // Ensure the return object has a AKV version tag, and Thumbprint if (!string.IsNullOrEmpty(cert.Properties.Version) && @@ -151,7 +156,7 @@ protected virtual JobResult PerformAddition(string alias, string pfxPassword, st #region Remove - protected virtual JobResult PerformRemoval(string alias, long jobHistoryId) + protected virtual JobResult PerformRemoval(string alias, string tagsJSON, long jobHistoryId) { JobResult complete = new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = jobHistoryId }; diff --git a/AzureKeyVault/PamUtilities.cs b/AzureKeyVault/PamUtilities.cs index 4b1e7c6..f85e29f 100644 --- a/AzureKeyVault/PamUtilities.cs +++ b/AzureKeyVault/PamUtilities.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using Keyfactor.Orchestrators.Extensions.Interfaces; using Microsoft.Extensions.Logging; diff --git a/CHANGELOG.md b/CHANGELOG.md index a4ecb84..f12ad78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +- 3.1.7 + - Added support for Azure KeyVault Certificate Metadata via Entry Parameters + - Converted to BouncyCastle crypto libraries + - Convert to .net6/8 dual build + - Update README to use doctool + - 3.1.6 - Preventing CertStore parameters from getting used if present but empty. - Improved trace logging diff --git a/integration-manifest.json b/integration-manifest.json index b9bd89e..a8a3ca1 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -36,7 +36,14 @@ "BlueprintAllowed": false, "Capability": "AKV", "CustomAliasAllowed": "Optional", - "EntryParameters": [], + "EntryParameters": [ + { + "Name": "CertificateTags", + "DisplayName": "Certificate Tags", + "Type": "string", + "DefaultValue": "" + } + ], "JobProperties": [], "LocalStore": false, "Name": "Azure Keyvault", diff --git a/readme_source.md b/readme_source.md index 10ee803..6019f67 100644 --- a/readme_source.md +++ b/readme_source.md @@ -456,6 +456,22 @@ Now we can navigate to the Keyfactor platform and create the store type for Azur you can limit the options to those that should be applicable to your organization. Refer to the [Azure Documentation](https://learn.microsoft.com/en-us/dotnet/api/azure.core.azurelocation?view=azure-dotnethttps://learn.microsoft.com/en-us/dotnet/api/azure.core.azurelocation?view=azure-dotnet) for a list of valid region names. If no value is selected, "eastus" is used by default. + +#### Certificate Tags (v3.1.7+) +If you would like to utilize Tags for associating arbitrary data with a Certificate in Azure KeyVault, you can utilize an entry parameter named "CertificateTags". + +Name: **"CertificateTags"** + +Display Name: **"Certificate Tags"** + +Type: **String** + +When supplied, this field should contain valid JSON representing a key-value dictionary. + +example: +```json +{{"myTag":"myTagValue"}, {"anotherTag":"anotherTagValue"}} +``` ### Install the Extension on the Orchestrator The process for installing an extension for the universal orchestrator differs from the process of installing an extension for the Windows orchestrator. Follow the below steps to register the Azure Keyvault integration with your instance of the universal orchestrator.