diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml index 6c03689..d906128 100644 --- a/.github/workflows/keyfactor-bootstrap-workflow.yml +++ b/.github/workflows/keyfactor-bootstrap-workflow.yml @@ -11,19 +11,9 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v4 - permissions: - contents: write # Explicitly grant write permission - with: - command_token_url: ${{ vars.COMMAND_TOKEN_URL }} - command_hostname: ${{ vars.COMMAND_HOSTNAME }} - command_base_api_path: ${{ vars.COMMAND_API_PATH }} + uses: keyfactor/actions/.github/workflows/starter.yml@v5 secrets: - token: ${{ secrets.V2BUILDTOKEN}} - gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} - gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} - scan_token: ${{ secrets.SAST_TOKEN }} - entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }} - entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }} - command_client_id: ${{ secrets.COMMAND_CLIENT_ID }} - command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }} + token: ${{ secrets.V2BUILDTOKEN}} # REQUIRED + gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} # Only required for golang builds + gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} # Only required for golang builds + scan_token: ${{ secrets.SAST_TOKEN }} # REQUIRED \ No newline at end of file diff --git a/AxisIPCamera/AxisIPCamera.csproj b/AxisIPCamera/AxisIPCamera.csproj index 020f565..44751e0 100644 --- a/AxisIPCamera/AxisIPCamera.csproj +++ b/AxisIPCamera/AxisIPCamera.csproj @@ -2,47 +2,54 @@ true - net6.0;net8.0 + net8.0;net10.0 true disable Keyfactor.Extensions.Orchestrator.AxisIPCamera + 1.1.0 - - - - - Always - - - - - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - + + + + + + + + + + + + + + + Always + + + + Always + + + + Always + + + + Always + + + + Always + + + + Always + + + + Always + diff --git a/AxisIPCamera/Client/AxisHttpClient.cs b/AxisIPCamera/Client/AxisHttpClient.cs index 7f594dc..103f3cd 100644 --- a/AxisIPCamera/Client/AxisHttpClient.cs +++ b/AxisIPCamera/Client/AxisHttpClient.cs @@ -1,4 +1,4 @@ -// Copyright 2025 Keyfactor +// Copyright 2026 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, @@ -524,30 +524,79 @@ public void RemoveCACertificate(string alias) Logger.LogError($"HTTP Request unsuccessful - HTTP Response: {DecodeHttpStatus(httpResponse)}"); throw new Exception($"HTTP Request unsuccessful."); } + // Decode the API response when HTTP response is successful + if (httpResponse != null && string.IsNullOrEmpty(httpResponse.Content)) + { + throw new Exception("No content returned from HTTP Response"); + } + + RestApiResponse apiResponse = JsonConvert.DeserializeObject(httpResponse.Content); + if (apiResponse.Status == Constants.Status.Success) + { + Logger.MethodExit(); + } else { - if (httpResponse != null && string.IsNullOrEmpty(httpResponse.Content)) - { - throw new Exception("No content returned from HTTP Response"); - } + ErrorData error = JsonConvert.DeserializeObject(httpResponse.Content); + throw new Exception( + $"API error encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})"); + } + } + catch (Exception e) + { + Logger.LogError("Error completing CA certificate remove: " + LogHandler.FlattenException(e)); + throw new Exception(e.Message); + } + } + + /// + /// Removes a certificate with private key from the device. + /// + /// Unique identifier of the CA certificate to be removed + public HttpResult RemoveCertificate(string alias) + { + try + { + Logger.MethodEntry(); + var context = new HttpContext(); + + var deleteCertResource = $"{Constants.RestApiEntryPoint}/certificates/{alias}"; + var httpResponse = ExecuteHttp(deleteCertResource, Method.Delete); + + // Decode the HTTP response if failed + if (httpResponse is { IsSuccessful: false }) + { + var decodedStatus = DecodeHttpStatus(httpResponse); + + Logger.LogWarning($"HTTP Request unsuccessful - HTTP Response: {decodedStatus}"); + context.AddWarning(decodedStatus); + } + + // Decode the API response for more information + if (httpResponse != null && string.IsNullOrEmpty(httpResponse.Content)) + { + Logger.LogError("No content returned from HTTP Response"); + context.AddError($"No content returned from HTTP Response for {nameof(Method.Delete)} {deleteCertResource}"); + } + else + { RestApiResponse apiResponse = JsonConvert.DeserializeObject(httpResponse.Content); - if (apiResponse.Status == Constants.Status.Success) - { - Logger.MethodExit(); - } - else + if (apiResponse.Status != Constants.Status.Success) { ErrorData error = JsonConvert.DeserializeObject(httpResponse.Content); - throw new Exception( - $"API error encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})"); + Logger.LogWarning($"API error encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})"); + context.AddWarning($"HTTP Request {nameof(Method.Delete)} {deleteCertResource}: API error encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})"); } } + + Logger.MethodExit(); + return context.ToResult(); } catch (Exception e) { - Logger.LogError("Error completing CA certificate remove: " + LogHandler.FlattenException(e)); + Logger.LogError("Error completing certificate remove: " + LogHandler.FlattenException(e)); throw new Exception(e.Message); } } diff --git a/AxisIPCamera/Helpers/DeviceCertValidator.cs b/AxisIPCamera/Helpers/DeviceCertValidator.cs index f24a662..f705a04 100644 --- a/AxisIPCamera/Helpers/DeviceCertValidator.cs +++ b/AxisIPCamera/Helpers/DeviceCertValidator.cs @@ -73,6 +73,12 @@ public static class DeviceCertValidator // Add TLS cert as leaf certificate to the end of the custom chain customChain.Add(parser.ReadCertificate(cert.RawData)); + + if (!File.Exists(trustedIntCertPath)) + { + logger.LogError($"{trustedIntCertPath} does not exist."); + return false; + } logger.LogTrace($"Loading Trusted Intermediate Certs from {trustedIntCertPath}"); var trustedIntCerts = parser.ReadCertificates(File.ReadAllBytes(trustedIntCertPath)); @@ -92,6 +98,12 @@ public static class DeviceCertValidator logger.LogTrace($"{trustedIntCerts.Count} Trusted Intermediate Certs found"); + if (!File.Exists(trustedRootCertPath)) + { + logger.LogError($"{trustedRootCertPath} does not exist."); + return false; + } + logger.LogTrace($"Loading Trusted Root Cert from {trustedRootCertPath}"); var trustedRootCerts = parser.ReadCertificates(File.ReadAllBytes(trustedRootCertPath)); @@ -214,8 +226,8 @@ private static bool VerifyAkiSkiChain(List customChain, ILogger { logger.MethodEntry(); - logger.LogTrace("Custom chain being validated includes: (1) Leaf cert from TLS session, (2) n-Intermediate certs from custom trust, &" + - "n-Root certs from custom trust"); + logger.LogTrace("Custom chain being validated includes: (1) Leaf cert from TLS session, (2) n-Intermediate certs from custom trust, & " + + "(3) n-Root certs from custom trust"); for (int i = 0; i < customChain.Count - 1; i++) { diff --git a/AxisIPCamera/Helpers/SANBuilder.cs b/AxisIPCamera/Helpers/SANBuilder.cs new file mode 100644 index 0000000..5b569d8 --- /dev/null +++ b/AxisIPCamera/Helpers/SANBuilder.cs @@ -0,0 +1,64 @@ +// Copyright 2026 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; +using System.Linq; +using Microsoft.Extensions.Logging; + +namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Helpers +{ + public static class SANBuilder + { + public static List BuildSANList(Dictionary sans, ILogger logger) + { + var parts = new List(); + + if (sans == null || sans.Count == 0) + { + logger.LogTrace($"SANs is null or empty"); + return parts; + } + + foreach (var entry in sans) + { + string key = NormalizeSanKey(entry.Key); + + // The Axis API only supports the addition of 'dns' and 'ip' SAN type keys + if (key is not ("DNS" or "IP")) + continue; + + if (entry.Value == null || entry.Value.Length == 0) + continue; + + // NOTE: We are separating the key and value pairs with a colon because this is the format + // required to send SANs to the Axis API endpoint + parts.AddRange( + entry.Value + .Where(v => !string.IsNullOrWhiteSpace(v)) + .Select(v => $"{key}:{v.Trim()}") + ); + } + + return parts; + } + + /// + /// Normalize SAN type keys to RFC-compliant names. + /// **NOTE: The Axis API only supports the addition of 'dns' and 'ip' SAN types. + /// Courtesy of B.Pokorny. + /// + private static string NormalizeSanKey(string key) + { + return key.Trim().ToLower() switch + { + "dns" => "DNS", + "ip" or "ip4" or "ip6" => "IP", + _ => key.ToLower() // default + }; + } + } +} \ No newline at end of file diff --git a/AxisIPCamera/Inventory.cs b/AxisIPCamera/Inventory.cs index ca579c9..921946a 100644 --- a/AxisIPCamera/Inventory.cs +++ b/AxisIPCamera/Inventory.cs @@ -48,7 +48,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd _logger.MethodEntry(); _logger.LogTrace($"Begin Inventory for Client Machine {config.CertificateStoreDetails.ClientMachine}..."); - string jsonConfig = JsonConvert.SerializeObject(config); + string jsonConfig = JsonConvert.SerializeObject(config, Formatting.Indented); _logger.LogDebug($"Inventory Config: {jsonConfig.Replace(config.ServerPassword,"**********")}"); _logger.LogTrace("Create HTTPS client to connect to device"); @@ -62,8 +62,9 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd _logger.LogTrace("Retrieve all client certificates"); CertificateData data2 = client.ListCertificates(); + // TODO: Remove this if not using // Get the default keystore - _logger.LogTrace("Retrieve the default keystore"); + /*_logger.LogTrace("Retrieve the default keystore"); Constants.Keystore defaultKeystore = client.GetDefaultKeystore(); string defaultKeystoreString = defaultKeystore.ToString(); _logger.LogDebug($"Inventory - Default keystore: {defaultKeystoreString}"); @@ -78,7 +79,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd foreach (var cert in data2.Certs.Where(cert => cert.Keystore == defaultKeystore)) { data2DefKey.Certs.Add(cert); - } + }*/ _logger.LogTrace("Retrieve all certificate bindings for each possible certificate usage type"); // Lookup the certificate used for HTTPS, MQTT, IEEE802.X @@ -88,7 +89,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd // Set the binding on the client certificates object if the aliases found for each certificate usage match _logger.LogTrace("Mark each client certificate with the appropriate certificate usage type"); - foreach (Certificate c in data2DefKey.Certs) + foreach (Certificate c in data2.Certs) { if (c.Alias.Equals(httpAlias)) { @@ -113,7 +114,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd } } - // Build the list of CA certificates and add to the InventoryItems object that is sent back to Command + // Build the list of CA certificates and add to the InventoryItems object sent back to Command inventoryItems.AddRange(data1.CACerts.Select( c => { @@ -130,8 +131,8 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd } }).Where(item => item?.Certificates != null).ToList()); - // Build the list of client certificates and add to the InventoryItems object that is sent back to Command - inventoryItems.AddRange(data2DefKey.Certs.Select( + // Build the list of client certificates and add to the InventoryItems object sent back to Command + inventoryItems.AddRange(data2.Certs.Select( c => { try diff --git a/AxisIPCamera/Management.cs b/AxisIPCamera/Management.cs index 357e4f3..62dcf9a 100644 --- a/AxisIPCamera/Management.cs +++ b/AxisIPCamera/Management.cs @@ -1,4 +1,4 @@ -// Copyright 2025 Keyfactor +// Copyright 2026 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, @@ -258,7 +258,11 @@ private static bool IsCACertificate(string certBase64Der) byte[] certBytes = Convert.FromBase64String(certBase64Der); // Create an X509 object so we can analyze the contents - X509Certificate2 certToAdd = new X509Certificate2(certBytes); + #if NET9_0_OR_GREATER + X509Certificate2 certToAdd = X509CertificateLoader.LoadCertificate(certBytes); + #else + X509Certificate2 certToAdd = new X509Certificate2(certBytes); + #endif foreach (X509Extension ext in certToAdd.Extensions) { diff --git a/AxisIPCamera/Model/Constants.cs b/AxisIPCamera/Model/Constants.cs index 2f182e0..7854103 100644 --- a/AxisIPCamera/Model/Constants.cs +++ b/AxisIPCamera/Model/Constants.cs @@ -1,4 +1,4 @@ -// Copyright 2025 Keyfactor +// Copyright 2026 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, @@ -6,7 +6,9 @@ // and limitations under the License. using System; +using System.Globalization; using System.IO; +using System.Text.RegularExpressions; using Newtonsoft.Json; using Org.BouncyCastle.OpenSsl; using Org.BouncyCastle.Pkcs; @@ -16,7 +18,7 @@ namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Model public static class Constants { // This is the API entry point for the REST VAPIX Cert Management API - public static string RestApiEntryPoint = "/config/rest/cert/v1beta"; + public static string RestApiEntryPoint = "/config/rest/cert/v1"; // This is the API entry point for the SOAP Cert Management API public static string SoapApiEntryPoint = "/vapix/services"; @@ -94,16 +96,16 @@ public static string MapKeyType(string keyAlgorithm, string keySize) { "RSA" when keySize == "2048" => "RSA-2048", "RSA" when keySize == "4096" => "RSA-4096", - "ECP" when keySize == "256" => "EC-P256", - "ECP" when keySize == "384" => "EC-P384", - "ECP" when keySize == "521" => "EC-P521", + "ECDSA" when keySize == "256" => "EC-P256", + "ECDSA" when keySize == "384" => "EC-P384", + "ECDSA" when keySize == "521" => "EC-P521", _ => "UNKNOWN" }; } /// /// Maps the certificate usage enum values to the corresponding string values that are configured - /// for the "Certificate Usage" entry parameter inside of Command. The string values *MUST* match + /// for the "Certificate Usage" entry parameter for the certificate store type in Command. The string values *MUST* match /// exactly the value configured for the certificate usage entry parameter in Command. /// /// @@ -248,4 +250,31 @@ public override void WriteJson( } } } + + public static class CertificateName + { + /// + /// Returns a UTC-based suffix, i.e. "2602171544" + /// + public static string GetUtcSuffix() => + DateTime.UtcNow.ToString("yyMMddHHmm", CultureInfo.InvariantCulture); + + /// + /// Creates a unique certificate name by appending ['_' + Utc DateTime suffix] to the end of the user-supplied certificate name. + /// Example: "_2602171544" + /// + public static string CreateUniqueCertName(string certName) + { + // check to see if the old cert name had a previously appended timestamp + // EDGE CASE: Scenario under which this could happen - Cert name bound to usage is known and used to schedule an ODKG job + Regex rgx = new Regex(@"_[0-9]{10}$",RegexOptions.CultureInvariant); + var m = Regex.Match(certName,@"_[0-9]{10}$"); + if (m.Success) + { + return certName.Remove(m.Index, m.Length) + "_" + GetUtcSuffix(); + } + + return certName + "_" + GetUtcSuffix(); + } + } } diff --git a/AxisIPCamera/Model/HttpResponse.cs b/AxisIPCamera/Model/HttpResponse.cs new file mode 100644 index 0000000..d1a0441 --- /dev/null +++ b/AxisIPCamera/Model/HttpResponse.cs @@ -0,0 +1,68 @@ +// Copyright 2026 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; + +namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Model +{ + public sealed class HttpContext + { + private readonly List _warnings = new(); + private readonly List _errors = new(); + + public IReadOnlyList Warnings => _warnings; + public IReadOnlyList Errors => _errors; + + public void AddWarning(string message) => + _warnings.Add(message); + + public void AddError(string message) => + _errors.Add(message); + + public HttpResult ToResult() + { + if (_errors.Any()) + return HttpResult.Error(FormatMessages(_errors)); + + if (_warnings.Any()) + return HttpResult.Warning(FormatMessages(_warnings)); + + return HttpResult.Success(); + } + + private static string FormatMessages(IEnumerable messages) + { + return string.Join( + Environment.NewLine, + messages.Select((message, index) => + $"({index + 1}) {message}") + ); + } + } + + public enum HttpStatus + { + Success, + Warning, + Error + } + + public sealed record HttpResult(HttpStatus Status, string Message = null) + { + public static HttpResult Success() + => new(HttpStatus.Success); + + public static HttpResult Warning(string message) + => new(HttpStatus.Warning, message); + + public static HttpResult Error(string message) + => new(HttpStatus.Error, message); + + } +} \ No newline at end of file diff --git a/AxisIPCamera/Reenrollment.cs b/AxisIPCamera/Reenrollment.cs index afd519b..b411815 100644 --- a/AxisIPCamera/Reenrollment.cs +++ b/AxisIPCamera/Reenrollment.cs @@ -1,4 +1,4 @@ -// Copyright 2025 Keyfactor +// Copyright 2026 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, @@ -7,6 +7,8 @@ using System; using System.Collections.Generic; +using System.Net; +using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; @@ -14,10 +16,14 @@ using Keyfactor.Logging; using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Client; +using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Helpers; using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Model; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Keyfactor.Orchestrators.Extensions.Interfaces; +using Keyfactor.PKI.Enums; +using Keyfactor.PKI.X509; +//using Org.BouncyCastle.X509; namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera { @@ -25,7 +31,7 @@ public class Reenrollment : IReenrollmentJobExtension { private readonly ILogger _logger; - private readonly IPAMSecretResolver _resolver; + private readonly IPAMSecretResolver _resolver; public string ExtensionName => ""; public Reenrollment(IPAMSecretResolver resolver) @@ -53,14 +59,30 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm } _logger.LogDebug("--- End Job Properties"); + // Log each SAN, if provided + _logger.LogDebug("Begin SANs ---"); + var formattedSANs = SANBuilder.BuildSANList(config.SANs,_logger); + if (formattedSANs.Count == 0) + { + _logger.LogDebug($"No SAN values found."); + } + else + { + foreach (var san in formattedSANs) + { + _logger.LogDebug($"{san}"); + } + } + _logger.LogDebug("--- End SANs"); + // Get required reenrollment fields string certUsage = config.JobProperties[Constants.CertUsageParamName].ToString() ?? throw new Exception($"{Constants.CertUsageParamDisplay} returned null"); var certUsageEnum = Constants.GetCertUsageAsEnum(certUsage); string keyAlgorithm = config.JobProperties["keyType"].ToString() ?? throw new Exception("Key Algorithm returned null"); string keySize = config.JobProperties["keySize"].ToString() ?? throw new Exception("Key Size returned null"); string subject = config.JobProperties["subjectText"].ToString() ?? throw new Exception("Subject returned null"); - string reenrollAlias = config.Alias ?? throw new Exception("Alias returned null"); - _logger.LogDebug($"Alias: {reenrollAlias}"); + string newAlias = config.Alias ?? throw new Exception("Alias returned null"); + _logger.LogDebug($"Alias: {newAlias}"); // Prevent reenrollment on Trust certificates if (certUsageEnum is Constants.CertificateUsage.Trust) @@ -72,28 +94,27 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm _logger.LogTrace("Create HTTPS client to connect to device"); var client = new AxisHttpClient(config, config.CertificateStoreDetails, _resolver); - // Get current binding for reenrollment certificate usage provided + // Get the existing alias name associated with the supplied cert usage _logger.LogTrace($"Check '{certUsage}' binding for same alias"); - var boundAlias = client.GetCertUsageBinding(Constants.GetCertUsageAsEnum(certUsage)); - if (!string.IsNullOrEmpty(boundAlias)) + var oldAlias = client.GetCertUsageBinding(Constants.GetCertUsageAsEnum(certUsage)); + var oldCertExists = false; + if (!string.IsNullOrEmpty(oldAlias)) { - _logger.LogDebug($"Alias currently bound to certificate usage type '{certUsage}': {boundAlias}"); + oldCertExists = true; + _logger.LogDebug($"Alias currently bound to certificate usage type '{certUsage}': {oldAlias}"); - if (boundAlias == reenrollAlias) - { - _logger.LogDebug($"Alias '{reenrollAlias}' provided for reenrollment matches alias '{boundAlias}' currently bound " + - $"to certificate usage type {certUsage}"); - - throw new Exception( - $"Alias '{reenrollAlias}' already exists for certificate usage type {certUsage}. Reenroll using another alias."); - } - - _logger.LogTrace($"Alias '{reenrollAlias}' provided for reenrollment differs from alias '{boundAlias}' currently bound " + - $"to certificate usage type {certUsage}. Proceeding..."); + // compare the old alias name with the new alias name --- + // 1) if the names are the same, append a reserved time-based suffix to the end of the name + // This new name [AliasA_Timestamp] will be used to create the new cert. + // OR + // 2) EDGE CASE: if the old alias name currently tied to the cert usage does NOT match the new alias name, + // also create a new name [CertB_Timestamp] for the new cert in case the user-supplied cert name is already + // associated with an existing certificate that is NOT bound to a cert usage + newAlias = CertificateName.CreateUniqueCertName(newAlias); } else { - _logger.LogDebug($"No alias currently bound to certificate usage type {certUsage}"); + _logger.LogDebug($"No alias currently bound to certificate usage type {certUsage}. Proceeding with new key, CSR, and adding cert for new alias..."); } // Map the key type and key size from the job properties to a corresponding key type available on the device @@ -112,10 +133,10 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm Constants.Keystore defaultKeystore = client.GetDefaultKeystore(); string defaultKeystoreString = defaultKeystore.ToString(); _logger.LogDebug($"Reenrollment - Default keystore: {defaultKeystoreString}"); - - _logger.LogTrace("Generating self-signed cert with private key on device"); - List sansList = new List(); - if (certUsageEnum == Constants.CertificateUsage.Https) + + // If no SANs are provided and the cert usage is 'HTTPS' --- + // Add 1 for DNS and 1 for IP address to eliminate TLS errors + if(formattedSANs.Count == 0 && certUsageEnum == Constants.CertificateUsage.Https) { _logger.LogTrace("Extracting CN and IP address to add as SANs to the certificate"); // Extract the CN from the Subject @@ -126,8 +147,9 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm throw new Exception( "No value provided in the Subject for 'CN'. This is required for HTTPS certificates."); } + _logger.LogTrace($"Extracted CN attribute from the Subject: {cnMatch.Groups[1].Value}"); - + // Extract the IP address from the Client Machine var ipMatch = Regex.Match(config.CertificateStoreDetails.ClientMachine, @"^(?(?:\d{1,3}\.){3}\d{1,3})", RegexOptions.IgnoreCase); @@ -137,25 +159,28 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm throw new Exception( "Value provided for the Client Machine does not match IPv4 format."); } - _logger.LogTrace($"Extracted IP Address from the Client Machine: { ipMatch.Groups["ip"].Value}"); - sansList.Add("DNS:" + cnMatch.Groups[1].Value); - sansList.Add("IP:" + ipMatch.Groups["ip"].Value); + _logger.LogTrace($"Extracted IP Address from the Client Machine: {ipMatch.Groups["ip"].Value}"); + + formattedSANs.Add($"DNS:{cnMatch.Groups[1].Value}"); + formattedSANs.Add($"IP:{ipMatch.Groups["ip"].Value}"); } - client.CreateSelfSignedCert(reenrollAlias,keyType,defaultKeystoreString,subject,sansList.ToArray()); - _logger.LogTrace("Obtaining CSR using self-signed certificate"); - var csr = client.ObtainCSR(reenrollAlias); + _logger.LogTrace("Generating private key pair on device"); + client.CreateSelfSignedCert(newAlias,keyType,defaultKeystoreString,subject,formattedSANs.ToArray()); + + _logger.LogTrace("Obtaining CSR"); + var csr = client.ObtainCSR(newAlias); _logger.LogDebug($"CSR: \n{csr}"); - + _logger.LogTrace("Validating CSR"); Constants.ValidateCsr(csr); _logger.LogTrace("CSR is valid"); - // Submit CSR to be signed in Keyfactor - _logger.LogTrace("Submitting CSR to be signed in Command"); + // Submit CSR to be signed + _logger.LogTrace("Submitting CSR to Command to enroll for signed certificate"); var x509Cert = submitReenrollment.Invoke(csr); - + // Build PEM content // ** NOTE: The static newline (\n) characters are required in the API request StringBuilder pemBuilder = new StringBuilder(); @@ -165,12 +190,41 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm pemBuilder.Append(noLineBreaks); pemBuilder.Append(@"\n-----END CERTIFICATE-----"); var pemCert = pemBuilder.ToString(); + + _logger.LogTrace($"Replacing cert '{newAlias}' with the following cert: " + pemCert); + client.ReplaceCertificate(newAlias,pemCert); + + _logger.LogTrace($"Setting '{certUsage}' binding to alias '{newAlias}'"); + client.SetCertUsageBinding(newAlias,certUsageEnum); + + // Perform unused certificate cleanup --- + // 1) If a bound alias exists, delete the bound alias + HttpResult result; + if (oldCertExists) + { + _logger.LogTrace($"Removing certificate and private key associated with alias '{oldAlias}'"); + result = client.RemoveCertificate(oldAlias); - _logger.LogTrace($"Replacing self-signed cert '{reenrollAlias}' with the following cert: " + pemCert); - client.ReplaceCertificate(reenrollAlias,pemCert); - - _logger.LogTrace($"Setting '{certUsage}' binding to alias '{reenrollAlias}'"); - client.SetCertUsageBinding(reenrollAlias, certUsageEnum); + if (result.Status == HttpStatus.Warning) + { + return new JobResult() { Result = OrchestratorJobStatusJobResult.Warning, JobHistoryId = config.JobHistoryId, + FailureMessage = $"Reenrollment Job Had Warnings - Refer to logs for more detailed information." }; + } + } + + + // TESTING build chain functionality + /*using var aiaClient = new HttpClient(); + var builder = new ChainBuilder(aiaClient); + var bcX509Cert = new X509CertificateParser().ReadCertificate(x509Cert.RawData); + var chain = builder.BuildChain(bcX509Cert, CertificateCollectionOrder.EndEntityFirst); + + int i = 0; + foreach (var cert in chain.Certificates) + { + i++; + _logger.LogTrace($"Cert {i}: {cert.SubjectDN.ToString()}"); + }*/ } catch (Exception ex) { diff --git a/CHANGELOG.md b/CHANGELOG.md index 12153e6..57dafb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +v1.1.0 +- chore(docs): Updated doctool release actions to v5 +- chore(build): Updated .NET target frameworks to net8.0 and net10.0 +- fix(odkg): Fixed incorrect mapping of ECDSA algorithm to ECP. Updated to ECDSA. +- feat(odkg): Implemented alias versioning to enable reuse of existing alias names in automation processes + v1.0.2 - fix(logs): Removed logging of plaintext cert store Server Password - fix(keystore): Updated Keystore type to be dynamic instead of a fixed Enum to allow compatibility across different cameras/firmware diff --git a/README.md b/README.md index 4e275e1..1562bcd 100644 --- a/README.md +++ b/README.md @@ -53,13 +53,12 @@ The Axis IP Camera Orchestrator extension DOES NOT support the following use cas \* Currently supported certificate usages include: **HTTPS**, **IEEE802.X**, **MQTT**, **Other** - - ## Compatibility This integration is compatible with Keyfactor Universal Orchestrator version 10.1 and later. ## Support + The AXIS IP Camera Universal Orchestrator extension is supported by Keyfactor. If you require support for any issues or have feature request, please open a support ticket by either contacting your Keyfactor representative or via the Keyfactor Support Portal at https://support.keyfactor.com. > If you want to contribute bug fixes or additional enhancements, use the **[Pull requests](../../pulls)** tab. @@ -68,19 +67,15 @@ The AXIS IP Camera Universal Orchestrator extension is supported by Keyfactor. I Before installing the AXIS IP Camera Universal Orchestrator extension, we recommend that you install [kfutil](https://github.com/Keyfactor/kfutil). Kfutil is a command-line tool that simplifies the process of creating store types, installing extensions, and instantiating certificate stores in Keyfactor Command. - 1. Out of the box, an AXIS IP Network Camera will typically have configured an **Administrator** account. It is recommended to create a new account specifically for executing API calls. This account will need \'Administrator\' privileges since the orchestrator extension is capable of making configuration changes, such as installing and removing certificates. 2. Currently supports AXIS M2035-LE Bullet Camera, AXIS OS version 12.2.62. Has not been tested with any other firmware version. - ## AxisIPCamera Certificate Store Type To use the AXIS IP Camera Universal Orchestrator extension, you **must** create the AxisIPCamera Certificate Store Type. This only needs to happen _once_ per Keyfactor Command instance. - - The AXIS IP Camera certificate store type represents a certificate store on an AXIS network camera that maintains two separate collections of certificates: * Client-server certificates (certs with private keys) @@ -88,32 +83,28 @@ that maintains two separate collections of certificates: It is expected that there be one (1) certificate store managed per AXIS network camera. - - - #### Axis IP Camera Requirements 1. User Account with \'Administrator\' privileges and password to access the camera 2. Camera serial number 3. Camera IP address (and likely port number) - - #### Supported Operations -| Operation | Is Supported | -|--------------|------------------------------------------------------------------------------------------------------------------------| -| Add | ✅ Checked | -| Remove | ✅ Checked | -| Discovery | 🔲 Unchecked | +| Operation | Is Supported | +|--------------|--------------| +| Add | ✅ Checked | +| Remove | ✅ Checked | +| Discovery | 🔲 Unchecked | | Reenrollment | ✅ Checked | -| Create | 🔲 Unchecked | +| Create | 🔲 Unchecked | #### Store Type Creation ##### Using kfutil: `kfutil` is a custom CLI for the Keyfactor Command API and can be used to create certificate store types. For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out the [docs](https://github.com/Keyfactor/kfutil?tab=readme-ov-file#quickstart) +
Click to expand AxisIPCamera kfutil details ##### Using online definition from GitHub: @@ -132,10 +123,10 @@ For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out ```
- #### Manual Creation Below are instructions on how to create the AxisIPCamera store type manually in the Keyfactor Command Portal +
Click to expand manual AxisIPCamera details Create a store type called `AxisIPCamera` with the attributes in the tables below: @@ -146,11 +137,11 @@ the Keyfactor Command Portal | Name | Axis IP Camera | Display name for the store type (may be customized) | | Short Name | AxisIPCamera | Short display name for the store type | | Capability | AxisIPCamera | Store type name orchestrator will register with. Check the box to allow entry of value | - | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add | - | Supports Remove | ✅ Checked | Check the box. Indicates that the Store Type supports Management Remove | - | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | - | Supports Reenrollment | ✅ Checked | Indicates that the Store Type supports Reenrollment | - | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | + | Supports Add | ✅ Checked | Indicates that the Store Type supports Management Add | + | Supports Remove | ✅ Checked | Indicates that the Store Type supports Management Remove | + | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | + | Supports Reenrollment | ✅ Checked | Indicates that the Store Type supports Reenrollment | + | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | | Needs Server | ✅ Checked | Determines if a target server name is required when creating store | | Blueprint Allowed | 🔲 Unchecked | Determines if store type may be included in an Orchestrator blueprint | | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell | @@ -159,18 +150,18 @@ the Keyfactor Command Portal The Basic tab should look like this: - ![AxisIPCamera Basic Tab](docsource/images/AxisIPCamera-basic-store-type-dialog.png) + ![AxisIPCamera Basic Tab](docsource/images/AxisIPCamera-basic-store-type-dialog.svg) ##### Advanced Tab | Attribute | Value | Description | | --------- | ----- | ----- | | Supports Custom Alias | Required | Determines if an individual entry within a store can have a custom Alias. | - | Private Key Handling | Forbidden | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. | + | Private Key Handling | Forbidden | This determines if Keyfactor can send the private key associated with a certificate to the store. | | PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) | The Advanced tab should look like this: - ![AxisIPCamera Advanced Tab](docsource/images/AxisIPCamera-advanced-store-type-dialog.png) + ![AxisIPCamera Advanced Tab](docsource/images/AxisIPCamera-advanced-store-type-dialog.svg) > For Keyfactor **Command versions 24.4 and later**, a Certificate Format dropdown is available with PFX and PEM options. Ensure that **PFX** is selected, as this determines the format of new and renewed certificates sent to the Orchestrator during a Management job. Currently, all Keyfactor-supported Orchestrator extensions support only PFX. @@ -185,8 +176,7 @@ the Keyfactor Command Portal The Custom Fields tab should look like this: - ![AxisIPCamera Custom Fields Tab](docsource/images/AxisIPCamera-custom-fields-store-type-dialog.png) - + ![AxisIPCamera Custom Fields Tab](docsource/images/AxisIPCamera-custom-fields-store-type-dialog.svg) ###### Server Username Enter the username of the configured "service" user on the camera @@ -196,8 +186,6 @@ the Keyfactor Command Portal > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. - - ###### Server Password Enter the password of the configured "service" user on the camera @@ -206,16 +194,11 @@ the Keyfactor Command Portal > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. - - ###### Use SSL Select True or False depending on if SSL (HTTPS) should be used to communicate with the camera. This should always be "True" - ![AxisIPCamera Custom Field - ServerUseSsl](docsource/images/AxisIPCamera-custom-field-ServerUseSsl-dialog.png) - ![AxisIPCamera Custom Field - ServerUseSsl](docsource/images/AxisIPCamera-custom-field-ServerUseSsl-validation-options-dialog.png) - - - + ![AxisIPCamera Custom Field - ServerUseSsl](docsource/images/AxisIPCamera-custom-field-ServerUseSsl-dialog.svg) + ![AxisIPCamera Custom Field - ServerUseSsl](docsource/images/AxisIPCamera-custom-field-ServerUseSsl-validation-options-dialog.svg) ##### Entry Parameters Tab @@ -226,15 +209,12 @@ the Keyfactor Command Portal The Entry Parameters tab should look like this: - ![AxisIPCamera Entry Parameters Tab](docsource/images/AxisIPCamera-entry-parameters-store-type-dialog.png) - - + ![AxisIPCamera Entry Parameters Tab](docsource/images/AxisIPCamera-entry-parameters-store-type-dialog.svg) ##### Certificate Usage The Certificate Usage to assign to the cert after enrollment. Can be left 'Other' to be assigned later. - ![AxisIPCamera Entry Parameter - CertUsage](docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage.png) - ![AxisIPCamera Entry Parameter - CertUsage](docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage-validation-options.png) - + ![AxisIPCamera Entry Parameter - CertUsage](docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage.svg) + ![AxisIPCamera Entry Parameter - CertUsage](docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage-validation-options.svg)
@@ -243,18 +223,16 @@ the Keyfactor Command Portal 1. **Download the latest AXIS IP Camera Universal Orchestrator extension from GitHub.** - Navigate to the [AXIS IP Camera Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/axis-ipcamera-orchestrator/releases/latest). Refer to the compatibility matrix below to determine the asset should be downloaded. Then, click the corresponding asset to download the zip archive. + Navigate to the [AXIS IP Camera Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/axis-ipcamera-orchestrator/releases/latest). Refer to the compatibility matrix below to determine which asset should be downloaded. Then, click the corresponding asset to download the zip archive. | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `axis-ipcamera-orchestrator` .NET version to download | | --------- | ----------- | ----------- | ----------- | - | Older than `11.0.0` | | | `net6.0` | - | Between `11.0.0` and `11.5.1` (inclusive) | `net6.0` | | `net6.0` | - | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Disable` | `net6.0` || Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | - | `11.6` _and_ newer | `net8.0` | | `net8.0` | + | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | + | `11.6` _and_ newer | `net8.0` | | `net8.0` | Unzip the archive containing extension assemblies to a known location. - > **Note** If you don't see an asset with a corresponding .NET version, you should always assume that it was compiled for `net6.0`. + > **Note** If you don't see an asset with a corresponding .NET version, you should always assume that it was compiled for `net8.0`. 2. **Locate the Universal Orchestrator extensions directory.** @@ -272,17 +250,14 @@ the Keyfactor Command Portal Refer to [Starting/Restarting the Universal Orchestrator service](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/StarttheService.htm). - 6. **(optional) PAM Integration** The AXIS IP Camera Universal Orchestrator extension is compatible with all supported Keyfactor PAM extensions to resolve PAM-eligible secrets. PAM extensions running on Universal Orchestrators enable secure retrieval of secrets from a connected PAM provider. To configure a PAM provider, [reference the Keyfactor Integration Catalog](https://keyfactor.github.io/integrations-catalog/content/pam) to select an extension and follow the associated instructions to install it on the Universal Orchestrator (remote). - > The above installation steps can be supplemented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/CustomExtensions.htm?Highlight=extensions). - ## Post Installation The AXIS IP Camera Orchestrator Extension *always* connects to an AXIS IP Network Camera via HTTPS, regardless @@ -321,11 +296,8 @@ These values must match or the session will be denied. > Therefore, you will need to install the full CA chain - including root and intermediate certificates - into the orchestrator server's local > certificate store. - ## Defining Certificate Stores - - ### Store Creation #### Manually with the Command UI @@ -340,8 +312,8 @@ These values must match or the session will be denied. Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- |---------------------------------------------------------| + | Attribute | Description | + | --------- | ----------- | | Category | Select "Axis IP Camera" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | The IP address of the Camera. Sample is "192.167.231.174:44444". Include the port if necessary. | @@ -353,8 +325,6 @@ These values must match or the session will be denied. - - #### Using kfutil CLI
Click to expand details @@ -387,7 +357,6 @@ These values must match or the session will be denied.
- #### PAM Provider Eligible Fields
Attributes eligible for retrieval by a PAM Provider on the Universal Orchestrator @@ -403,10 +372,8 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov
- > The content in this section can be supplemented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store). - ### Certificate Usage Every certificate inventoried will have an Entry Parameter called \`Certificate Usage\`. @@ -432,27 +399,57 @@ There are five (5) possible options: 5. Other - This certificate usage identifies all other certificates on the camera that do not fall under the pre-defined usages above. -> [!NOTE] -> A Reenrollment (ODKG) job will not allow enrollment of certificates with **Trust** assigned as the \`Certificate Usage\`. -> Trust CA certificates can be added to the camera via a Management - Add job. + +## Enrollment Behavior + +The following enrollment behaviors are specific to AXIS cameras and should be considered when designing certificate automation workflows. + +### Alias Versioning + +AXIS cameras require each Alias to be unique, and each Alias is tightly coupled with the private key used to generate its certificate. Because of this, certificates cannot be reenrolled in place by replacing the certificate and private key associated with an existing Alias. + +To support certificate renewals and automation workflows, the orchestrator generates a unique Alias by appending the following suffix: + +`_yyMMddHHmm` + +where `yyMMddHHmm` represents the current UTC date and time. + +From an automation perspective, the same Alias can continue to be reused, as uniqueness is enforced by the integration. > [!NOTE] -> For a Reenrollment (ODKG) job, where the \`Certificate Usage\` assigned is **HTTPS**, IP and DNS are added as SANS -> to the enrolled certificate. -> -> IP = Client Machine configured for the certificate store (excluding any port) -> -> DNS = CN set in the Subject DN +> As of v1.1.0, Reenrollment jobs automatically manage versioned Aliases. When reenrolling a certificate using the same Alias, the integration creates a new certificate using a versioned Alias and removes the previously enrolled certificate associated with the same base Alias. +> +> Because AXIS cameras do not support in-place certificate replacement, a new versioned Alias is still created during reenrollment. The integration identifies and removes the previous certificate by matching the base Alias name and ignoring the timestamp suffix. +> +> If a new Alias is supplied during reenrollment, the original certificate (if one exists) associated with the selected `Certificate Usage` is **not** automatically removed from the camera. Because AXIS cameras have limited certificate and key storage capacity, users should periodically review and remove unused certificates through the AXIS Network Camera GUI. + +### Certificate Usage Considerations (Trust) +> [!NOTE] +> If a Reenrollment (ODKG) job is configured with `Trust` selected as the `Certificate Usage`, the job will return a warning indicating that the operation is not supported. +> +> Trust CA certificates must be installed using a **Management - Add** job. These certificates establish trust for TLS connections initiated by the camera. +### Subject Alternative Names (SANs) -## Caveats +As of Keyfactor Command v25.4, Subject Alternative Names (SANs) can be specified for Reenrollment (ODKG) jobs. Support for passing SANs to the orchestrator also requires, at minimum, Keyfactor Universal Orchestrator v25.1. -> [!NOTE] -> Reenrollment jobs will not replace or remove a client-server certificate with the same alias. They will also not remove -> the original certificate if a particular \`Certificate Usage\` had an associated cert. Since the camera has limited storage, -> it will be up to the user to remove any unused client-server certificates via the AXIS Network Camera GUI. +The AXIS IP Camera API only supports DNS and IP SAN types. Any other SAN types included in the Reenrollment job will be ignored and will not be added to the enrolled certificate. + +> [!NOTE] +> If SANs are not provided and the selected `Certificate Usage` is `HTTPS`, IP and DNS SANs are automatically added when enrolling a certificate associated with a new Alias: +> +> - **IP** = The Client Machine configured for the certificate store (excluding any port number) +> - **DNS** = The Common Name (CN) specified in the certificate Subject DN + +## Operational Notes + +### AXIS OS 12 Firmware Recommendation + +> [!IMPORTANT] +> Devices running the AXIS OS 12 release track should always be updated to the latest firmware version available from Axis. Previous firmware versions are no longer supported once a newer release becomes available. +Axis identified a memory leak in older firmware releases that may cause device keystore storage to become exhausted over time. If keystore storage issues are observed, verify that the device is running the latest supported firmware version. ## License @@ -460,4 +457,4 @@ Apache License 2.0, see [LICENSE](LICENSE). ## Related Integrations -See all [Keyfactor Universal Orchestrator extensions](https://github.com/orgs/Keyfactor/repositories?q=orchestrator). \ No newline at end of file +See all [Keyfactor Universal Orchestrator extensions](https://github.com/orgs/Keyfactor/repositories?q=orchestrator). diff --git a/docsource/axisipcamera.md b/docsource/axisipcamera.md index af3aed2..8e048bc 100644 --- a/docsource/axisipcamera.md +++ b/docsource/axisipcamera.md @@ -36,16 +36,4 @@ There are five (5) possible options: 4. Trust - This certificate usage describes a public certificate issued by a CA used to establish trust. 5. Other - - This certificate usage identifies all other certificates on the camera that do not fall under the pre-defined usages above. - -> [!NOTE] -> A Reenrollment (ODKG) job will not allow enrollment of certificates with **Trust** assigned as the \`Certificate Usage\`. -> Trust CA certificates can be added to the camera via a Management - Add job. - -> [!NOTE] -> For a Reenrollment (ODKG) job, where the \`Certificate Usage\` assigned is **HTTPS**, IP and DNS are added as SANS -> to the enrolled certificate. -> -> IP = Client Machine configured for the certificate store (excluding any port) -> -> DNS = CN set in the Subject DN \ No newline at end of file + - This certificate usage identifies all other certificates on the camera that do not fall under the pre-defined usages above. \ No newline at end of file diff --git a/docsource/content.md b/docsource/content.md index 24e31b2..9fddf60 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -22,6 +22,49 @@ The Axis IP Camera Orchestrator extension DOES NOT support the following use cas \* Currently supported certificate usages include: **HTTPS**, **IEEE802.X**, **MQTT**, **Other** +## Enrollment Behavior + +The following enrollment behaviors are specific to AXIS cameras and should be considered when designing certificate automation workflows. + +### Alias Versioning + +AXIS cameras require each Alias to be unique, and each Alias is tightly coupled with the private key used to generate its certificate. Because of this, certificates cannot be reenrolled in place by replacing the certificate and private key associated with an existing Alias. + +To support certificate renewals and automation workflows, the orchestrator generates a unique Alias by appending the following suffix: + +`_yyMMddHHmm` + +where `yyMMddHHmm` represents the current UTC date and time. + +From an automation perspective, the same Alias can continue to be reused, as uniqueness is enforced by the integration. + +> [!NOTE] +> As of v1.1.0, Reenrollment jobs automatically manage versioned Aliases. When reenrolling a certificate using the same Alias, the integration creates a new certificate using a versioned Alias and removes the previously enrolled certificate associated with the same base Alias. +> +> Because AXIS cameras do not support in-place certificate replacement, a new versioned Alias is still created during reenrollment. The integration identifies and removes the previous certificate by matching the base Alias name and ignoring the timestamp suffix. +> +> If a new Alias is supplied during reenrollment, the original certificate (if one exists) associated with the selected `Certificate Usage` is **not** automatically removed from the camera. Because AXIS cameras have limited certificate and key storage capacity, users should periodically review and remove unused certificates through the AXIS Network Camera GUI. + +### Certificate Usage Considerations (Trust) + +> [!NOTE] +> If a Reenrollment (ODKG) job is configured with `Trust` selected as the `Certificate Usage`, the job will return a warning indicating that the operation is not supported. +> +> Trust CA certificates must be installed using a **Management - Add** job. These certificates establish trust for TLS connections initiated by the camera. + +### Subject Alternative Names (SANs) + +As of Keyfactor Command v25.4, Subject Alternative Names (SANs) can be specified for Reenrollment (ODKG) jobs. Support for passing SANs to the orchestrator also requires, at minimum, Keyfactor Universal Orchestrator v25.1. + +The AXIS IP Camera API only supports DNS and IP SAN types. Any other SAN types included in the Reenrollment job will be ignored and will not be added to the enrolled certificate. + +> [!NOTE] +> If SANs are not provided and the selected `Certificate Usage` is `HTTPS`, IP and DNS SANs are automatically added when enrolling a certificate associated with a new Alias: +> +> - **IP** = The Client Machine configured for the certificate store (excluding any port number) +> - **DNS** = The Common Name (CN) specified in the certificate Subject DN + + ## Requirements 1. Out of the box, an AXIS IP Network Camera will typically have configured an **Administrator** account. It is @@ -67,9 +110,12 @@ These values must match or the session will be denied. > Therefore, you will need to install the full CA chain - including root and intermediate certificates - into the orchestrator server's local > certificate store. -## Caveats +## Operational Notes + +### AXIS OS 12 Firmware Recommendation + +> [!IMPORTANT] +> Devices running the AXIS OS 12 release track should always be updated to the latest firmware version available from Axis. Previous firmware versions are no longer supported once a newer release becomes available. -> [!NOTE] -> Reenrollment jobs will not replace or remove a client-server certificate with the same alias. They will also not remove -> the original certificate if a particular \`Certificate Usage\` had an associated cert. Since the camera has limited storage, -> it will be up to the user to remove any unused client-server certificates via the AXIS Network Camera GUI. +Axis identified a memory leak in older firmware releases that may cause device keystore storage to become exhausted over time. If keystore storage issues are observed, verify that the device is running the latest supported firmware version. + diff --git a/docsource/images/AxisIPCamera-advanced-store-type-dialog.svg b/docsource/images/AxisIPCamera-advanced-store-type-dialog.svg new file mode 100644 index 0000000..6957ae3 --- /dev/null +++ b/docsource/images/AxisIPCamera-advanced-store-type-dialog.svg @@ -0,0 +1,67 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + + Custom Fields + Entry Parameters + + + + + Store Path Type + + + + Freeform + + Fixed + + Multiple Choice + + + + + Other Settings + + Supports Custom Alias + + Forbidden + + Optional + + + Required + Private Key Handling + + + Forbidden + + Optional + + Required + PFX Password Style + + + Default + + Custom + \ No newline at end of file diff --git a/docsource/images/AxisIPCamera-basic-store-type-dialog.svg b/docsource/images/AxisIPCamera-basic-store-type-dialog.svg new file mode 100644 index 0000000..664fc3a --- /dev/null +++ b/docsource/images/AxisIPCamera-basic-store-type-dialog.svg @@ -0,0 +1,84 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + + Advanced + Custom Fields + Entry Parameters + + + + + Details + + Name + + Axis IP Camera + Short Name + + AxisIPCamera + Custom Capability + + + Custom Capability + + + + Supported Job Types + + + + Inventory + + + Add + + + Remove + + Create + + Discovery + + + ODKG + + + + General Settings + + + + Needs Server + + Blueprint Allowed + + Uses PowerShell + + + + Password Settings + + + Requires Store Password + + Supports Entry Password + \ No newline at end of file diff --git a/docsource/images/AxisIPCamera-custom-field-ServerUseSsl-dialog.svg b/docsource/images/AxisIPCamera-custom-field-ServerUseSsl-dialog.svg new file mode 100644 index 0000000..08ec420 --- /dev/null +++ b/docsource/images/AxisIPCamera-custom-field-ServerUseSsl-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + ServerUseSsl + Display Name + + Use SSL + Type + + Bool + + Default Value + + + True + + False + + Not Set + Depends On + + + Server Username + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AxisIPCamera-custom-field-ServerUseSsl-validation-options-dialog.svg b/docsource/images/AxisIPCamera-custom-field-ServerUseSsl-validation-options-dialog.svg new file mode 100644 index 0000000..7993c23 --- /dev/null +++ b/docsource/images/AxisIPCamera-custom-field-ServerUseSsl-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AxisIPCamera-custom-fields-store-type-dialog.svg b/docsource/images/AxisIPCamera-custom-fields-store-type-dialog.svg new file mode 100644 index 0000000..2108f8a --- /dev/null +++ b/docsource/images/AxisIPCamera-custom-fields-store-type-dialog.svg @@ -0,0 +1,71 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + Custom Fields + + Entry Parameters + + + + + + ADD + + EDIT + + DELETE + Total: 3 + + + Display Name + Type + Default Value / Options + + + + + + + + + + + Server Username + Secret + + + + + + + Server Password + Secret + + + + + + + Use SSL + Bool + true + \ No newline at end of file diff --git a/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage-validation-options.svg b/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage-validation-options.svg new file mode 100644 index 0000000..c85b2b8 --- /dev/null +++ b/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + Optional + + + Required + + Hidden + Removing an entry + + + Optional + + Required + + Hidden + On Device Key Generation + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage.svg b/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage.svg new file mode 100644 index 0000000..08b4668 --- /dev/null +++ b/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage.svg @@ -0,0 +1,51 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + CertUsage + Display Name + + Certificate Usage + Type + + MultipleChoice + + Default Value + + + Multiple Choice Options + + HTTPS,IEEE802.X,MQTT,Trust,Other + Depends On + + + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog.svg b/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog.svg new file mode 100644 index 0000000..6e2e97f --- /dev/null +++ b/docsource/images/AxisIPCamera-entry-parameters-store-type-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + Custom Fields + Entry Parameters + + + + + + + ADD + + EDIT + + DELETE + Total: 1 + + + Display Name + Type + Default Value + + + + + + + + + + + Certificate Usage + MultipleChoice + \ No newline at end of file diff --git a/integration-manifest.json b/integration-manifest.json index 4c1144a..f4a1cbc 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -27,7 +27,6 @@ "PrivateKeyAllowed": "Forbidden", "SupportedOperations": { "Add": true, - "Create": false, "Discovery": false, "Enrollment": true, "Remove": true diff --git a/scripts/store_types/bash/curl_create_store_types.sh b/scripts/store_types/bash/curl_create_store_types.sh new file mode 100755 index 0000000..97f4d97 --- /dev/null +++ b/scripts/store_types/bash/curl_create_store_types.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# Store Type creation script using curl +# Generated by Doctool + +set -e + +# Configuration - set these variables before running +KEYFACTOR_HOSTNAME="${KEYFACTOR_HOSTNAME}" +KEYFACTOR_API_PATH="${KEYFACTOR_API_PATH:-KeyfactorAPI}" +KEYFACTOR_AUTH_TOKEN="${KEYFACTOR_AUTH_TOKEN}" + +echo "Creating store type: AxisIPCamera" +curl -s -X POST "https://${KEYFACTOR_HOSTNAME}/${KEYFACTOR_API_PATH}/CertificateStoreTypes" \ + -H "Authorization: Bearer ${KEYFACTOR_AUTH_TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-keyfactor-requested-with: APIClient" \ + -d '{ + "Name": "Axis IP Camera", + "ShortName": "AxisIPCamera", + "Capability": "AxisIPCamera", + "ServerRequired": true, + "BlueprintAllowed": false, + "PowerShell": false, + "CustomAliasAllowed": "Required", + "PrivateKeyAllowed": "Forbidden", + "SupportedOperations": { + "Add": true, + "Discovery": false, + "Enrollment": true, + "Remove": true + }, + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "Properties": [ + { + "Name": "ServerUsername", + "DisplayName": "Server Username", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": true, + "Description": "Enter the username of the configured \"service\" user on the camera" + }, + { + "Name": "ServerPassword", + "DisplayName": "Server Password", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": true, + "Description": "Enter the password of the configured \"service\" user on the camera" + }, + { + "Name": "ServerUseSsl", + "DisplayName": "Use SSL", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "true", + "Required": true, + "Description": "Select True or False depending on if SSL (HTTPS) should be used to communicate with the camera. This should always be \"True\"" + } + ], + "EntryParameters": [ + { + "Name": "CertUsage", + "DisplayName": "Certificate Usage", + "Type": "MultipleChoice", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": true, + "OnRemove": false, + "OnReenrollment": true + }, + "Options": "HTTPS,IEEE802.X,MQTT,Trust,Other", + "Description": "The Certificate Usage to assign to the cert after enrollment. Can be left 'Other' to be assigned later." + } + ], + "StorePathType": "", + "StorePathValue": "", + "JobProperties": [] +}' + diff --git a/scripts/store_types/bash/kfutil_create_store_types.sh b/scripts/store_types/bash/kfutil_create_store_types.sh new file mode 100755 index 0000000..aecc07d --- /dev/null +++ b/scripts/store_types/bash/kfutil_create_store_types.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Store Type creation script using kfutil +# Generated by Doctool + +set -e + +echo "Creating store type: AxisIPCamera" +kfutil store-types create AxisIPCamera + diff --git a/scripts/store_types/powershell/kfutil_create_store_types.ps1 b/scripts/store_types/powershell/kfutil_create_store_types.ps1 new file mode 100644 index 0000000..59f0203 --- /dev/null +++ b/scripts/store_types/powershell/kfutil_create_store_types.ps1 @@ -0,0 +1,6 @@ +# Store Type creation script using kfutil +# Generated by Doctool + +Write-Host "Creating store type: AxisIPCamera" +kfutil store-types create AxisIPCamera + diff --git a/scripts/store_types/powershell/restmethod_create_store_types.ps1 b/scripts/store_types/powershell/restmethod_create_store_types.ps1 new file mode 100644 index 0000000..1c90eed --- /dev/null +++ b/scripts/store_types/powershell/restmethod_create_store_types.ps1 @@ -0,0 +1,88 @@ +# Store Type creation script using Invoke-RestMethod +# Generated by Doctool + +# Configuration - set these variables before running +$KeyfactorHostname = $env:KEYFACTOR_HOSTNAME +$KeyfactorApiPath = if ($env:KEYFACTOR_API_PATH) { $env:KEYFACTOR_API_PATH } else { "KeyfactorAPI" } +$KeyfactorAuthToken = $env:KEYFACTOR_AUTH_TOKEN + +$Headers = @{ + "Authorization" = "Bearer $KeyfactorAuthToken" + "Content-Type" = "application/json" + "x-keyfactor-requested-with" = "APIClient" +} + +Write-Host "Creating store type: AxisIPCamera" +$Body = @' +{ + "Name": "Axis IP Camera", + "ShortName": "AxisIPCamera", + "Capability": "AxisIPCamera", + "ServerRequired": true, + "BlueprintAllowed": false, + "PowerShell": false, + "CustomAliasAllowed": "Required", + "PrivateKeyAllowed": "Forbidden", + "SupportedOperations": { + "Add": true, + "Discovery": false, + "Enrollment": true, + "Remove": true + }, + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "Properties": [ + { + "Name": "ServerUsername", + "DisplayName": "Server Username", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": true, + "Description": "Enter the username of the configured \"service\" user on the camera" + }, + { + "Name": "ServerPassword", + "DisplayName": "Server Password", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": true, + "Description": "Enter the password of the configured \"service\" user on the camera" + }, + { + "Name": "ServerUseSsl", + "DisplayName": "Use SSL", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "true", + "Required": true, + "Description": "Select True or False depending on if SSL (HTTPS) should be used to communicate with the camera. This should always be \"True\"" + } + ], + "EntryParameters": [ + { + "Name": "CertUsage", + "DisplayName": "Certificate Usage", + "Type": "MultipleChoice", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": true, + "OnRemove": false, + "OnReenrollment": true + }, + "Options": "HTTPS,IEEE802.X,MQTT,Trust,Other", + "Description": "The Certificate Usage to assign to the cert after enrollment. Can be left 'Other' to be assigned later." + } + ], + "StorePathType": "", + "StorePathValue": "", + "JobProperties": [] +} +'@ + +Invoke-RestMethod -Uri "https://$KeyfactorHostname/$KeyfactorApiPath/CertificateStoreTypes" -Method POST -Headers $Headers -Body $Body +