From 984ab17062ec9c37331e7237edcc5b1d4d36f8d4 Mon Sep 17 00:00:00 2001 From: Elena Fiocca Date: Thu, 16 Apr 2026 18:40:07 -0600 Subject: [PATCH 01/10] Save local changes --- AxisIPCamera/AxisIPCamera.csproj | 77 ++++++++++--------- AxisIPCamera/Client/AxisHttpClient.cs | 42 ++++++++--- AxisIPCamera/Helpers/DeviceCertValidator.cs | 16 +++- AxisIPCamera/Helpers/SANBuilder.cs | 64 ++++++++++++++++ AxisIPCamera/Reenrollment.cs | 84 +++++++++++++++------ docsource/axisipcamera.md | 14 +++- docsource/content.md | 8 +- 7 files changed, 229 insertions(+), 76 deletions(-) create mode 100644 AxisIPCamera/Helpers/SANBuilder.cs diff --git a/AxisIPCamera/AxisIPCamera.csproj b/AxisIPCamera/AxisIPCamera.csproj index 020f565..13b30a3 100644 --- a/AxisIPCamera/AxisIPCamera.csproj +++ b/AxisIPCamera/AxisIPCamera.csproj @@ -2,47 +2,54 @@ true - net6.0;net8.0 + net8.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..2bbe950 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, @@ -6,6 +6,7 @@ // and limitations under the License. using System; +using System.Collections.Generic; using System.Reflection; using System.IO; using System.Linq; @@ -285,8 +286,7 @@ public Constants.Keystore GetDefaultKeystore() /// Combination of key algorithm and key size /// Default keystore for the device /// Subject provided for the certificate - /// Subject Alternative Names - public void CreateSelfSignedCert(string alias, string keyType, string keystore, string subject, string[] sans) + public void CreateSelfSignedCert(string alias, string keyType, string keystore, string subject) { try { @@ -303,7 +303,7 @@ public void CreateSelfSignedCert(string alias, string keyType, string keystore, KeyType = keyType, Keystore = keystore, Subject = subject, - SANS = sans, + SANS = [], ValidFrom = 0, // Cert validity period will be determined by the template ValidTo = 0 // Cert validity period will be determined by the template } @@ -347,12 +347,15 @@ public void CreateSelfSignedCert(string alias, string keyType, string keystore, } /// - /// Obtains a CSR for the self-signed certificate with private key on the device. - /// Fields from the self-signed certificate will be copied into the CSR. + /// Obtains a CSR for the self-signed or existing certificate with private key on the device. + /// Fields from the certificate will be copied into the CSR. + /// SANs will be added to the CSR. /// /// Unique identifier for the cert to be generated from the CSR + /// Subject provided for the certificate + /// Subject Alternative Names /// CSR string - public string ObtainCSR(string alias) + public string ObtainCSR(string alias, string subject, List sans) { try { @@ -360,12 +363,29 @@ public string ObtainCSR(string alias) var postCSRResource = $"{Constants.RestApiEntryPoint}/certificates/{alias}/get_csr"; - // Compose the body --- This is required, but leaving the contents blank. - // All information obtained in the self-signed cert will be used to create the CSR. + // Compose the body --- This is required. + // All information obtained in the self-signed or existing cert will be used to create the CSR. // If there are attributes assigned by the CA, those will override the attributes that end up // in the certificate signed by the CA. - string jsonBody = @"{""data"":{}}"; - var httpResponse = ExecuteHttp(postCSRResource, Method.Post, Constants.ApiType.Rest, jsonBody); + // 1. If a field is filled out, that value will be used in the CSR + // 2. If a field is NOT filled out, the existing value from the existing certificate will be copied into the CSR + // 3. If a field is filled out with a blank value, that field is not copied from the existing certificate nor added to the CSR + var jsonBody = new StringBuilder(@"{""data"":{"); + + if (sans.Count == 0) + { + jsonBody.Append(@"""subject"":""").Append(subject).Append("}}"); + } + else + { + jsonBody.Append(@"""subject"":""").Append(subject).Append(@""",""subject_alt_names"": ["); + string result = string.Join(",", sans); + jsonBody.Append(result).Append("]}}"); + } + + Logger.LogDebug($"POST Request Body: {jsonBody}"); + + var httpResponse = ExecuteHttp(postCSRResource, Method.Post, Constants.ApiType.Rest, jsonBody.ToString()); // Decode the HTTP response if failed if (httpResponse is {IsSuccessful:false}) 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..e539e82 --- /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 BuildSANString(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/Reenrollment.cs b/AxisIPCamera/Reenrollment.cs index afd519b..716028b 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,7 @@ using System; using System.Collections.Generic; +using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; @@ -14,10 +15,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 +30,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,6 +58,22 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm } _logger.LogDebug("--- End Job Properties"); + // Log each SAN, if provided + _logger.LogDebug("Begin SANs ---"); + var formattedSANs = SANBuilder.BuildSANString(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); @@ -75,6 +96,7 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm // Get current binding for reenrollment certificate usage provided _logger.LogTrace($"Check '{certUsage}' binding for same alias"); var boundAlias = client.GetCertUsageBinding(Constants.GetCertUsageAsEnum(certUsage)); + var replaceCert = false; if (!string.IsNullOrEmpty(boundAlias)) { _logger.LogDebug($"Alias currently bound to certificate usage type '{certUsage}': {boundAlias}"); @@ -82,14 +104,14 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm 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."); + $"to certificate usage type {certUsage}. Proceeding with rekeying, CSR, and replacing cert for alias..."); + replaceCert = true; + } + else + { + _logger.LogTrace($"Alias '{reenrollAlias}' provided for reenrollment differs from alias '{boundAlias}' currently bound " + + $"to certificate usage type {certUsage}. Proceeding with new key, CSR, and adding cert for alias..."); } - - _logger.LogTrace($"Alias '{reenrollAlias}' provided for reenrollment differs from alias '{boundAlias}' currently bound " + - $"to certificate usage type {certUsage}. Proceeding..."); } else { @@ -112,10 +134,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' and this is a new alias --- + // Add 1 for DNS and 1 for IP address to eliminate TLS errors + if(formattedSANs.Count == 0 && certUsageEnum == Constants.CertificateUsage.Https && !replaceCert) { _logger.LogTrace("Extracting CN and IP address to add as SANs to the certificate"); // Extract the CN from the Subject @@ -126,8 +148,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,15 +160,21 @@ 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}"""); + } + + if (!replaceCert) + { + _logger.LogTrace("Generating self-signed cert with private key on device"); + client.CreateSelfSignedCert(reenrollAlias,keyType,defaultKeystoreString,subject); } - client.CreateSelfSignedCert(reenrollAlias,keyType,defaultKeystoreString,subject,sansList.ToArray()); - _logger.LogTrace("Obtaining CSR using self-signed certificate"); - var csr = client.ObtainCSR(reenrollAlias); + _logger.LogTrace("Obtaining CSR"); + var csr = client.ObtainCSR(reenrollAlias,subject,formattedSANs); _logger.LogDebug($"CSR: \n{csr}"); _logger.LogTrace("Validating CSR"); @@ -155,6 +184,19 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm // Submit CSR to be signed in Keyfactor _logger.LogTrace("Submitting CSR to be signed in Command"); var x509Cert = submitReenrollment.Invoke(csr); + + // 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()}"); + }*/ // Build PEM content // ** NOTE: The static newline (\n) characters are required in the API request @@ -166,7 +208,7 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm pemBuilder.Append(@"\n-----END CERTIFICATE-----"); var pemCert = pemBuilder.ToString(); - _logger.LogTrace($"Replacing self-signed cert '{reenrollAlias}' with the following cert: " + pemCert); + _logger.LogTrace($"Replacing cert '{reenrollAlias}' with the following cert: " + pemCert); client.ReplaceCertificate(reenrollAlias,pemCert); _logger.LogTrace($"Setting '{certUsage}' binding to alias '{reenrollAlias}'"); diff --git a/docsource/axisipcamera.md b/docsource/axisipcamera.md index af3aed2..1971ad6 100644 --- a/docsource/axisipcamera.md +++ b/docsource/axisipcamera.md @@ -41,11 +41,17 @@ There are five (5) possible options: > [!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. +> These CA certificates establish trust for TLS connections initiated by the camera. > [!NOTE] -> For a Reenrollment (ODKG) job, where the \`Certificate Usage\` assigned is **HTTPS**, IP and DNS are added as SANS -> to the enrolled certificate. +> As of Keyfactor Command v25.4, SANs can be provided for a Reenrollment (ODKG) job. +> You must also have installed, at minimum, the Keyfactor Universal Orchestator v25.1 +> in order for the SANs to be sent to the orchestrator. > -> IP = Client Machine configured for the certificate store (excluding any port) +> The Axis IP Camera API *only* supports the addition of DNS and IP SANs. If you add other SAN types to the ODKG job, these will be ignored and not added to the certificate. > -> DNS = CN set in the Subject DN \ No newline at end of file +> * If SANs are NOT provided and the \`Certificate Usage\` assigned is **HTTPS**, IP and DNS will be automatically added as SANs to an enrolled certificate associated with a NEW alias. +> +> * IP = Client Machine configured for the certificate store (excluding any port) +> +> * DNS = CN set in the Subject DN \ No newline at end of file diff --git a/docsource/content.md b/docsource/content.md index 24e31b2..338b04e 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -70,6 +70,8 @@ These values must match or the session will be denied. ## Caveats > [!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. +> v1.1.0 - A Reenrollment job will now replace the certificate contents for an existing alias on the camera, therefore, not requiring +> a new alias be supplied for every new certificate enrollment. +> +> If a new alias is supplied, a Reenrollment job will not remove the original certificate associated with the \`Certificate Usage\`. +> Since the camera has limited storage, it will be up to the user to remove any unused certificates via the AXIS Network Camera GUI. From 8c632ce30480fe7ef172d73501f86af66ad98c00 Mon Sep 17 00:00:00 2001 From: Elena Fiocca Date: Tue, 5 May 2026 19:10:44 -0600 Subject: [PATCH 02/10] Save local changes --- AxisIPCamera/Client/AxisHttpClient.cs | 111 +++++++++++++++++--------- AxisIPCamera/Helpers/SANBuilder.cs | 4 +- AxisIPCamera/Model/Constants.cs | 33 +++++++- AxisIPCamera/Reenrollment.cs | 105 ++++++++++++------------ 4 files changed, 160 insertions(+), 93 deletions(-) diff --git a/AxisIPCamera/Client/AxisHttpClient.cs b/AxisIPCamera/Client/AxisHttpClient.cs index 2bbe950..72f661d 100644 --- a/AxisIPCamera/Client/AxisHttpClient.cs +++ b/AxisIPCamera/Client/AxisHttpClient.cs @@ -6,7 +6,6 @@ // and limitations under the License. using System; -using System.Collections.Generic; using System.Reflection; using System.IO; using System.Linq; @@ -286,7 +285,8 @@ public Constants.Keystore GetDefaultKeystore() /// Combination of key algorithm and key size /// Default keystore for the device /// Subject provided for the certificate - public void CreateSelfSignedCert(string alias, string keyType, string keystore, string subject) + /// Subject Alternative Names + public void CreateSelfSignedCert(string alias, string keyType, string keystore, string subject, string[] sans) { try { @@ -303,7 +303,7 @@ public void CreateSelfSignedCert(string alias, string keyType, string keystore, KeyType = keyType, Keystore = keystore, Subject = subject, - SANS = [], + SANS = sans, ValidFrom = 0, // Cert validity period will be determined by the template ValidTo = 0 // Cert validity period will be determined by the template } @@ -347,15 +347,12 @@ public void CreateSelfSignedCert(string alias, string keyType, string keystore, } /// - /// Obtains a CSR for the self-signed or existing certificate with private key on the device. - /// Fields from the certificate will be copied into the CSR. - /// SANs will be added to the CSR. + /// Obtains a CSR for the self-signed certificate with private key on the device. + /// Fields from the self-signed certificate will be copied into the CSR. /// /// Unique identifier for the cert to be generated from the CSR - /// Subject provided for the certificate - /// Subject Alternative Names /// CSR string - public string ObtainCSR(string alias, string subject, List sans) + public string ObtainCSR(string alias) { try { @@ -363,29 +360,12 @@ public string ObtainCSR(string alias, string subject, List sans) var postCSRResource = $"{Constants.RestApiEntryPoint}/certificates/{alias}/get_csr"; - // Compose the body --- This is required. - // All information obtained in the self-signed or existing cert will be used to create the CSR. + // Compose the body --- This is required, but leaving the contents blank. + // All information obtained in the self-signed cert will be used to create the CSR. // If there are attributes assigned by the CA, those will override the attributes that end up // in the certificate signed by the CA. - // 1. If a field is filled out, that value will be used in the CSR - // 2. If a field is NOT filled out, the existing value from the existing certificate will be copied into the CSR - // 3. If a field is filled out with a blank value, that field is not copied from the existing certificate nor added to the CSR - var jsonBody = new StringBuilder(@"{""data"":{"); - - if (sans.Count == 0) - { - jsonBody.Append(@"""subject"":""").Append(subject).Append("}}"); - } - else - { - jsonBody.Append(@"""subject"":""").Append(subject).Append(@""",""subject_alt_names"": ["); - string result = string.Join(",", sans); - jsonBody.Append(result).Append("]}}"); - } - - Logger.LogDebug($"POST Request Body: {jsonBody}"); - - var httpResponse = ExecuteHttp(postCSRResource, Method.Post, Constants.ApiType.Rest, jsonBody.ToString()); + string jsonBody = @"{""data"":{}}"; + var httpResponse = ExecuteHttp(postCSRResource, Method.Post, Constants.ApiType.Rest, jsonBody); // Decode the HTTP response if failed if (httpResponse is {IsSuccessful:false}) @@ -544,22 +524,75 @@ 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 void RemoveCertificate(string alias) + { + try + { + Logger.MethodEntry(); - RestApiResponse apiResponse = JsonConvert.DeserializeObject(httpResponse.Content); - if (apiResponse.Status == Constants.Status.Success) + var deleteCertResource = $"{Constants.RestApiEntryPoint}/certificates/{alias}"; + var httpResponse = ExecuteHttp(deleteCertResource, Method.Delete); + + // Decode the HTTP response if failed + if (httpResponse is { IsSuccessful: false }) + { + 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 + { + ErrorData error = JsonConvert.DeserializeObject(httpResponse.Content); + + // Check for error code 5 - "Validation error: Certificate is in use" or "Validation error: Certificate is not deletable" --- + // This will capture all device ID certs, which we do not want to delete anyway + if (error.ErrorInfo is { Code: 5, Message: "Validation error: Certificate is not deletable" or "Validation error: Certificate is in use"}) { - Logger.MethodExit(); + Logger.LogWarning($"API warning encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})"); } else { - ErrorData error = JsonConvert.DeserializeObject(httpResponse.Content); throw new Exception( $"API error encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})"); } @@ -567,7 +600,7 @@ public void RemoveCACertificate(string alias) } 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/SANBuilder.cs b/AxisIPCamera/Helpers/SANBuilder.cs index e539e82..5b569d8 100644 --- a/AxisIPCamera/Helpers/SANBuilder.cs +++ b/AxisIPCamera/Helpers/SANBuilder.cs @@ -13,7 +13,7 @@ namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Helpers { public static class SANBuilder { - public static List BuildSANString(Dictionary sans, ILogger logger) + public static List BuildSANList(Dictionary sans, ILogger logger) { var parts = new List(); @@ -39,7 +39,7 @@ public static List BuildSANString(Dictionary sans, ILo parts.AddRange( entry.Value .Where(v => !string.IsNullOrWhiteSpace(v)) - .Select(v => $@"""{key}:{v.Trim()}""") + .Select(v => $"{key}:{v.Trim()}") ); } diff --git a/AxisIPCamera/Model/Constants.cs b/AxisIPCamera/Model/Constants.cs index 2f182e0..9a0f9ad 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"; @@ -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/Reenrollment.cs b/AxisIPCamera/Reenrollment.cs index 716028b..ed5d275 100644 --- a/AxisIPCamera/Reenrollment.cs +++ b/AxisIPCamera/Reenrollment.cs @@ -60,7 +60,7 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm // Log each SAN, if provided _logger.LogDebug("Begin SANs ---"); - var formattedSANs = SANBuilder.BuildSANString(config.SANs,_logger); + var formattedSANs = SANBuilder.BuildSANList(config.SANs,_logger); if (formattedSANs.Count == 0) { _logger.LogDebug($"No SAN values found."); @@ -80,8 +80,8 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm 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) @@ -93,29 +93,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)); - var replaceCert = false; - 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}. Proceeding with rekeying, CSR, and replacing cert for alias..."); - replaceCert = true; - } - else - { - _logger.LogTrace($"Alias '{reenrollAlias}' provided for reenrollment differs from alias '{boundAlias}' currently bound " + - $"to certificate usage type {certUsage}. Proceeding with new key, CSR, and adding cert for alias..."); - } + // 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 @@ -135,9 +133,9 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm string defaultKeystoreString = defaultKeystore.ToString(); _logger.LogDebug($"Reenrollment - Default keystore: {defaultKeystoreString}"); - // If no SANs are provided and the cert usage is 'HTTPS' and this is a new alias --- + // 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 && !replaceCert) + 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 @@ -163,28 +161,51 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm _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}"""); - } - - if (!replaceCert) - { - _logger.LogTrace("Generating self-signed cert with private key on device"); - client.CreateSelfSignedCert(reenrollAlias,keyType,defaultKeystoreString,subject); + formattedSANs.Add($"DNS:{cnMatch.Groups[1].Value}"); + formattedSANs.Add($"IP:{ipMatch.Groups["ip"].Value}"); } + _logger.LogTrace("Generating private key pair on device"); + client.CreateSelfSignedCert(newAlias,keyType,defaultKeystoreString,subject,formattedSANs.ToArray()); + _logger.LogTrace("Obtaining CSR"); - var csr = client.ObtainCSR(reenrollAlias,subject,formattedSANs); + 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(); + pemBuilder.Append(@"-----BEGIN CERTIFICATE-----\n"); + string s = Convert.ToBase64String(x509Cert.RawData, Base64FormattingOptions.InsertLineBreaks); + var noLineBreaks = s.Replace(Environment.NewLine,@"\n"); + 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 and there is a new alias to enroll, delete the bound alias + if (oldCertExists) + { + _logger.LogTrace($"Removing certificate and private key associated with alias '{oldAlias}'"); + client.RemoveCertificate(oldAlias); + } + + // TODO: Add check for device ID + // TESTING build chain functionality /*using var aiaClient = new HttpClient(); var builder = new ChainBuilder(aiaClient); @@ -197,22 +218,6 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm i++; _logger.LogTrace($"Cert {i}: {cert.SubjectDN.ToString()}"); }*/ - - // Build PEM content - // ** NOTE: The static newline (\n) characters are required in the API request - StringBuilder pemBuilder = new StringBuilder(); - pemBuilder.Append(@"-----BEGIN CERTIFICATE-----\n"); - string s = Convert.ToBase64String(x509Cert.RawData, Base64FormattingOptions.InsertLineBreaks); - var noLineBreaks = s.Replace(Environment.NewLine,@"\n"); - pemBuilder.Append(noLineBreaks); - pemBuilder.Append(@"\n-----END CERTIFICATE-----"); - var pemCert = pemBuilder.ToString(); - - _logger.LogTrace($"Replacing cert '{reenrollAlias}' with the following cert: " + pemCert); - client.ReplaceCertificate(reenrollAlias,pemCert); - - _logger.LogTrace($"Setting '{certUsage}' binding to alias '{reenrollAlias}'"); - client.SetCertUsageBinding(reenrollAlias, certUsageEnum); } catch (Exception ex) { From 0192565d3d59483cf0616e93d6cece5f12540c78 Mon Sep 17 00:00:00 2001 From: Elena Fiocca Date: Tue, 5 May 2026 19:15:05 -0600 Subject: [PATCH 03/10] Save local changes --- AxisIPCamera/Reenrollment.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/AxisIPCamera/Reenrollment.cs b/AxisIPCamera/Reenrollment.cs index ed5d275..77cf765 100644 --- a/AxisIPCamera/Reenrollment.cs +++ b/AxisIPCamera/Reenrollment.cs @@ -204,8 +204,6 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm client.RemoveCertificate(oldAlias); } - // TODO: Add check for device ID - // TESTING build chain functionality /*using var aiaClient = new HttpClient(); var builder = new ChainBuilder(aiaClient); From 1cb29d2f7fa2a98844975a70f6f11456d384e66e Mon Sep 17 00:00:00 2001 From: Elena Fiocca Date: Wed, 6 May 2026 13:19:50 -0600 Subject: [PATCH 04/10] Save local changes --- AxisIPCamera/Client/AxisHttpClient.cs | 42 ++++++++--------- AxisIPCamera/Inventory.cs | 9 ++-- AxisIPCamera/Model/HttpResponse.cs | 68 +++++++++++++++++++++++++++ AxisIPCamera/Reenrollment.cs | 13 ++++- 4 files changed, 103 insertions(+), 29 deletions(-) create mode 100644 AxisIPCamera/Model/HttpResponse.cs diff --git a/AxisIPCamera/Client/AxisHttpClient.cs b/AxisIPCamera/Client/AxisHttpClient.cs index 72f661d..103f3cd 100644 --- a/AxisIPCamera/Client/AxisHttpClient.cs +++ b/AxisIPCamera/Client/AxisHttpClient.cs @@ -549,54 +549,50 @@ public void RemoveCACertificate(string alias) throw new Exception(e.Message); } } - + /// /// Removes a certificate with private key from the device. /// /// Unique identifier of the CA certificate to be removed - public void RemoveCertificate(string alias) + 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 }) { - Logger.LogError($"HTTP Request unsuccessful - HTTP Response: {DecodeHttpStatus(httpResponse)}"); - throw new Exception($"HTTP Request unsuccessful."); + var decodedStatus = DecodeHttpStatus(httpResponse); + + Logger.LogWarning($"HTTP Request unsuccessful - HTTP Response: {decodedStatus}"); + context.AddWarning(decodedStatus); } - // Decode the API response when HTTP response is successful + // Decode the API response for more information 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(); + Logger.LogError("No content returned from HTTP Response"); + context.AddError($"No content returned from HTTP Response for {nameof(Method.Delete)} {deleteCertResource}"); } else { - ErrorData error = JsonConvert.DeserializeObject(httpResponse.Content); - - // Check for error code 5 - "Validation error: Certificate is in use" or "Validation error: Certificate is not deletable" --- - // This will capture all device ID certs, which we do not want to delete anyway - if (error.ErrorInfo is { Code: 5, Message: "Validation error: Certificate is not deletable" or "Validation error: Certificate is in use"}) - { - Logger.LogWarning($"API warning encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})"); - } - else + RestApiResponse apiResponse = JsonConvert.DeserializeObject(httpResponse.Content); + if (apiResponse.Status != Constants.Status.Success) { - throw new Exception( - $"API error encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})"); + ErrorData error = JsonConvert.DeserializeObject(httpResponse.Content); + 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) { diff --git a/AxisIPCamera/Inventory.cs b/AxisIPCamera/Inventory.cs index ca579c9..278eb3c 100644 --- a/AxisIPCamera/Inventory.cs +++ b/AxisIPCamera/Inventory.cs @@ -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)) { @@ -131,7 +132,7 @@ 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( + inventoryItems.AddRange(data2.Certs.Select( c => { try 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 77cf765..b411815 100644 --- a/AxisIPCamera/Reenrollment.cs +++ b/AxisIPCamera/Reenrollment.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; @@ -197,13 +198,21 @@ public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollm client.SetCertUsageBinding(newAlias,certUsageEnum); // Perform unused certificate cleanup --- - // 1) If a bound alias exists and there is a new alias to enroll, delete the bound alias + // 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}'"); - client.RemoveCertificate(oldAlias); + result = client.RemoveCertificate(oldAlias); + + 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); From 494741ea8ab58a397062ef9b6b6538865d1bbfc6 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Thu, 7 May 2026 18:25:41 +0000 Subject: [PATCH 05/10] Update generated docs --- README.md | 28 ++-- .../bash/curl_create_store_types.sh | 128 ++++++++++++++++++ .../bash/kfutil_create_store_types.sh | 28 ++++ .../powershell/kfutil_create_store_types.ps1 | 29 ++++ .../restmethod_create_store_types.ps1 | 122 +++++++++++++++++ 5 files changed, 324 insertions(+), 11 deletions(-) create mode 100755 scripts/store_types/bash/curl_create_store_types.sh create mode 100755 scripts/store_types/bash/kfutil_create_store_types.sh create mode 100644 scripts/store_types/powershell/kfutil_create_store_types.ps1 create mode 100644 scripts/store_types/powershell/restmethod_create_store_types.ps1 diff --git a/README.md b/README.md index 4e275e1..53c69fa 100644 --- a/README.md +++ b/README.md @@ -247,14 +247,12 @@ the Keyfactor Command Portal | 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` | + | 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.** @@ -435,23 +433,31 @@ There are five (5) possible options: > [!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. +> These CA certificates establish trust for TLS connections initiated by the camera. > [!NOTE] -> For a Reenrollment (ODKG) job, where the \`Certificate Usage\` assigned is **HTTPS**, IP and DNS are added as SANS -> to the enrolled certificate. +> As of Keyfactor Command v25.4, SANs can be provided for a Reenrollment (ODKG) job. +> You must also have installed, at minimum, the Keyfactor Universal Orchestator v25.1 +> in order for the SANs to be sent to the orchestrator. > -> IP = Client Machine configured for the certificate store (excluding any port) +> The Axis IP Camera API *only* supports the addition of DNS and IP SANs. If you add other SAN types to the ODKG job, these will be ignored and not added to the certificate. > -> DNS = CN set in the Subject DN +> * If SANs are NOT provided and the \`Certificate Usage\` assigned is **HTTPS**, IP and DNS will be automatically added as SANs to an enrolled certificate associated with a NEW alias. +> +> * IP = Client Machine configured for the certificate store (excluding any port) +> +> * DNS = CN set in the Subject DN ## Caveats > [!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. +> v1.1.0 - A Reenrollment job will now replace the certificate contents for an existing alias on the camera, therefore, not requiring +> a new alias be supplied for every new certificate enrollment. +> +> If a new alias is supplied, a Reenrollment job will not remove the original certificate associated with the \`Certificate Usage\`. +> Since the camera has limited storage, it will be up to the user to remove any unused certificates via the AXIS Network Camera GUI. ## License 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..3ab9e04 --- /dev/null +++ b/scripts/store_types/bash/curl_create_store_types.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash + +# Creates all 1 store types via the Keyfactor Command REST API using curl. +# +# Authentication (first matching method is used): +# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN +# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET +# + KEYFACTOR_AUTH_TOKEN_URL +# Basic auth (AD): KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN +# +# Always required: +# KEYFACTOR_HOSTNAME Command hostname (e.g. my-command.example.com) +# +# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. + +if [ -z "${KEYFACTOR_HOSTNAME}" ]; then + echo "ERROR: KEYFACTOR_HOSTNAME is required" + exit 1 +fi + +BASE_URL="https://${KEYFACTOR_HOSTNAME}/keyfactorapi" + +# --------------------------------------------------------------------------- +# Resolve auth +# --------------------------------------------------------------------------- +if [ -n "${KEYFACTOR_AUTH_ACCESS_TOKEN}" ]; then + BEARER_TOKEN="${KEYFACTOR_AUTH_ACCESS_TOKEN}" +elif [ -n "${KEYFACTOR_AUTH_CLIENT_ID}" ] && [ -n "${KEYFACTOR_AUTH_CLIENT_SECRET}" ] && [ -n "${KEYFACTOR_AUTH_TOKEN_URL}" ]; then + echo "Fetching OAuth token..." + BEARER_TOKEN=$(curl -s -X POST "${KEYFACTOR_AUTH_TOKEN_URL}" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=${KEYFACTOR_AUTH_CLIENT_ID}" \ + --data-urlencode "client_secret=${KEYFACTOR_AUTH_CLIENT_SECRET}" | jq -r '.access_token') + if [ -z "${BEARER_TOKEN}" ] || [ "${BEARER_TOKEN}" = "null" ]; then + echo "ERROR: Failed to fetch OAuth token from ${KEYFACTOR_AUTH_TOKEN_URL}" + exit 1 + fi +elif [ -n "${KEYFACTOR_USERNAME}" ] && [ -n "${KEYFACTOR_PASSWORD}" ] && [ -n "${KEYFACTOR_DOMAIN}" ]; then + BEARER_TOKEN="" +else + echo "ERROR: Authentication required. Set one of:" + echo " KEYFACTOR_AUTH_ACCESS_TOKEN" + echo " KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET + KEYFACTOR_AUTH_TOKEN_URL" + echo " KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN" + exit 1 +fi + +if [ -n "${BEARER_TOKEN}" ]; then + CURL_AUTH=("-H" "Authorization: Bearer ${BEARER_TOKEN}") +else + CURL_AUTH=("-u" "${KEYFACTOR_USERNAME}@${KEYFACTOR_DOMAIN}:${KEYFACTOR_PASSWORD}") +fi + +create_store_type() { + local name="$1" + local body="$2" + echo "Creating ${name} store type..." + response=$(curl -s -o /dev/null -w "%{http_code}" \ + -X POST "${BASE_URL}/certificatestoretypes" \ + -H "Content-Type: application/json" \ + -H "x-keyfactor-requested-with: APIClient" \ + "${CURL_AUTH[@]}" \ + -d "${body}") + if [ "$response" = "200" ] || [ "$response" = "201" ]; then + echo " OK (HTTP ${response})" + else + echo " FAILED (HTTP ${response})" + fi +} + +# --------------------------------------------------------------------------- +# AxisIPCamera — The IP address of the Camera. Sample is "192.167.231.174:44444". Include the port if necessary. +# --------------------------------------------------------------------------- +create_store_type "AxisIPCamera" '{ + "Name": "Axis IP Camera", + "ShortName": "AxisIPCamera", + "Capability": "AxisIPCamera", + "ServerRequired": true, + "BlueprintAllowed": false, + "PowerShell": false, + "CustomAliasAllowed": "Required", + "PrivateKeyAllowed": "Forbidden", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": true, + "Remove": true + }, + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "Properties": [ + { + "Name": "ServerUseSsl", + "DisplayName": "Use SSL", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "true", + "Required": 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." + } + ], + "StorePathDescription": "Enter the Serial Number of the camera e.g. `0b7c3d2f9e8a`", + "StorePathType": "", + "StorePathValue": "", + "JobProperties": [] +}' + + +echo "Completed." 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..27dfa46 --- /dev/null +++ b/scripts/store_types/bash/kfutil_create_store_types.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Creates all 1 store types using kfutil. +# kfutil reads definitions from the Keyfactor integration catalog. +# +# Auth environment variables (first matching method is used): +# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN +# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET +# + KEYFACTOR_AUTH_TOKEN_URL +# Basic auth (AD): KEYFACTOR_HOSTNAME + KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD +# + KEYFACTOR_DOMAIN +# +# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. + +if ! command -v kfutil &> /dev/null; then + echo "kfutil could not be found. Please install kfutil" + echo "See https://github.com/Keyfactor/kfutil#quickstart" + exit 1 +fi + +if [ -z "$KEYFACTOR_HOSTNAME" ]; then + echo "KEYFACTOR_HOSTNAME not set — launching kfutil login" + kfutil login +fi + +kfutil store-types create --name "AxisIPCamera" + +echo "Done. All store types created." 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..7280dd5 --- /dev/null +++ b/scripts/store_types/powershell/kfutil_create_store_types.ps1 @@ -0,0 +1,29 @@ +# Creates all 1 store types using kfutil. +# kfutil reads definitions from the Keyfactor integration catalog. +# +# Auth environment variables (first matching method is used): +# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN +# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET +# + KEYFACTOR_AUTH_TOKEN_URL +# Basic auth (AD): KEYFACTOR_HOSTNAME + KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD +# + KEYFACTOR_DOMAIN +# +# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. + +# Uncomment if kfutil is not in your PATH +# Set-Alias -Name kfutil -Value 'C:\Program Files\Keyfactor\kfutil\kfutil.exe' + +if ($null -eq (Get-Command "kfutil" -ErrorAction SilentlyContinue)) { + Write-Host "kfutil could not be found. Please install kfutil" + Write-Host "See https://github.com/Keyfactor/kfutil#quickstart" + exit 1 +} + +if (-not $env:KEYFACTOR_HOSTNAME) { + Write-Host "KEYFACTOR_HOSTNAME not set — launching kfutil login" + & kfutil login +} + +& kfutil store-types create --name "AxisIPCamera" + +Write-Host "Done. All store types created." 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..e23ce6f --- /dev/null +++ b/scripts/store_types/powershell/restmethod_create_store_types.ps1 @@ -0,0 +1,122 @@ +# Creates all 1 store types via the Keyfactor Command REST API +# using PowerShell Invoke-RestMethod. +# +# Authentication (first matching method is used): +# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN +# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET +# + KEYFACTOR_AUTH_TOKEN_URL +# Basic auth (AD): KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN +# +# Always required: +# KEYFACTOR_HOSTNAME Command hostname (e.g. my-command.example.com) +# +# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. + +if (-not $env:KEYFACTOR_HOSTNAME) { + Write-Error "KEYFACTOR_HOSTNAME is required" + exit 1 +} + +$uri = "https://$($env:KEYFACTOR_HOSTNAME)/keyfactorapi/certificatestoretypes" +$headers = @{ + 'Content-Type' = "application/json" + 'x-keyfactor-requested-with' = "APIClient" +} + +# --------------------------------------------------------------------------- +# Resolve auth +# --------------------------------------------------------------------------- +if ($env:KEYFACTOR_AUTH_ACCESS_TOKEN) { + $headers['Authorization'] = "Bearer $($env:KEYFACTOR_AUTH_ACCESS_TOKEN)" +} elseif ($env:KEYFACTOR_AUTH_CLIENT_ID -and $env:KEYFACTOR_AUTH_CLIENT_SECRET -and $env:KEYFACTOR_AUTH_TOKEN_URL) { + Write-Host "Fetching OAuth token..." + $tokenBody = @{ + grant_type = 'client_credentials' + client_id = $env:KEYFACTOR_AUTH_CLIENT_ID + client_secret = $env:KEYFACTOR_AUTH_CLIENT_SECRET + } + $tokenResp = Invoke-RestMethod -Method Post -Uri $env:KEYFACTOR_AUTH_TOKEN_URL -Body $tokenBody + $headers['Authorization'] = "Bearer $($tokenResp.access_token)" +} elseif ($env:KEYFACTOR_USERNAME -and $env:KEYFACTOR_PASSWORD -and $env:KEYFACTOR_DOMAIN) { + $cred = [System.Convert]::ToBase64String( + [System.Text.Encoding]::ASCII.GetBytes( + "$($env:KEYFACTOR_USERNAME)@$($env:KEYFACTOR_DOMAIN):$($env:KEYFACTOR_PASSWORD)")) + $headers['Authorization'] = "Basic $cred" +} else { + Write-Error ("Authentication required. Set one of:`n" + + " KEYFACTOR_AUTH_ACCESS_TOKEN`n" + + " KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET + KEYFACTOR_AUTH_TOKEN_URL`n" + + " KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN") + exit 1 +} + +function New-StoreType { + param([string]$Name, [string]$Body) + Write-Host "Creating $Name store type..." + try { + Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -Body $Body -ContentType "application/json" | Out-Null + Write-Host " OK" + } catch { + Write-Warning " FAILED: $($_.Exception.Message)" + } +} + +# --------------------------------------------------------------------------- +# AxisIPCamera — The IP address of the Camera. Sample is "192.167.231.174:44444". Include the port if necessary. +# --------------------------------------------------------------------------- +New-StoreType "AxisIPCamera" @' +{ + "Name": "Axis IP Camera", + "ShortName": "AxisIPCamera", + "Capability": "AxisIPCamera", + "ServerRequired": true, + "BlueprintAllowed": false, + "PowerShell": false, + "CustomAliasAllowed": "Required", + "PrivateKeyAllowed": "Forbidden", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": true, + "Remove": true + }, + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "Properties": [ + { + "Name": "ServerUseSsl", + "DisplayName": "Use SSL", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "true", + "Required": 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." + } + ], + "StorePathDescription": "Enter the Serial Number of the camera e.g. `0b7c3d2f9e8a`", + "StorePathType": "", + "StorePathValue": "", + "JobProperties": [] +} +'@ + + +Write-Host "Completed." From 803ee9b309e457b1b71e3a56d8f3031420a92ede Mon Sep 17 00:00:00 2001 From: Elena Fiocca Date: Thu, 2 Jul 2026 14:23:01 -0600 Subject: [PATCH 06/10] Update build and release tooling; fixed ECDSA algorithm mapping --- .../keyfactor-bootstrap-workflow.yml | 20 +++++-------------- AxisIPCamera/AxisIPCamera.csproj | 2 +- AxisIPCamera/Inventory.cs | 6 +++--- AxisIPCamera/Management.cs | 8 ++++++-- AxisIPCamera/Model/Constants.cs | 8 ++++---- CHANGELOG.md | 6 ++++++ integration-manifest.json | 1 - 7 files changed, 25 insertions(+), 26 deletions(-) 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 13b30a3..44751e0 100644 --- a/AxisIPCamera/AxisIPCamera.csproj +++ b/AxisIPCamera/AxisIPCamera.csproj @@ -2,7 +2,7 @@ true - net8.0 + net8.0;net10.0 true disable Keyfactor.Extensions.Orchestrator.AxisIPCamera diff --git a/AxisIPCamera/Inventory.cs b/AxisIPCamera/Inventory.cs index 278eb3c..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"); @@ -114,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 => { @@ -131,7 +131,7 @@ 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 + // Build the list of client certificates and add to the InventoryItems object sent back to Command inventoryItems.AddRange(data2.Certs.Select( c => { 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 9a0f9ad..7854103 100644 --- a/AxisIPCamera/Model/Constants.cs +++ b/AxisIPCamera/Model/Constants.cs @@ -96,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. /// /// 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/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 From 54d96211e7cb3dc3d279c4b12e2c8e6fc6268ccc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 2 Jul 2026 20:23:41 +0000 Subject: [PATCH 07/10] docs: auto-generate README and documentation [skip ci] --- README.md | 87 ++++---------- ...xisIPCamera-advanced-store-type-dialog.svg | 67 +++++++++++ .../AxisIPCamera-basic-store-type-dialog.svg | 84 +++++++++++++ ...amera-custom-field-ServerUseSsl-dialog.svg | 54 +++++++++ ...ServerUseSsl-validation-options-dialog.svg | 39 ++++++ ...Camera-custom-fields-store-type-dialog.svg | 71 +++++++++++ ...pe-dialog-CertUsage-validation-options.svg | 68 +++++++++++ ...parameters-store-type-dialog-CertUsage.svg | 51 ++++++++ ...era-entry-parameters-store-type-dialog.svg | 54 +++++++++ .../bash/curl_create_store_types.sh | 111 ++++++------------ .../bash/kfutil_create_store_types.sh | 31 +---- .../powershell/kfutil_create_store_types.ps1 | 31 +---- .../restmethod_create_store_types.ps1 | 100 ++++++---------- 13 files changed, 591 insertions(+), 257 deletions(-) create mode 100644 docsource/images/AxisIPCamera-advanced-store-type-dialog.svg create mode 100644 docsource/images/AxisIPCamera-basic-store-type-dialog.svg create mode 100644 docsource/images/AxisIPCamera-custom-field-ServerUseSsl-dialog.svg create mode 100644 docsource/images/AxisIPCamera-custom-field-ServerUseSsl-validation-options-dialog.svg create mode 100644 docsource/images/AxisIPCamera-custom-fields-store-type-dialog.svg create mode 100644 docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage-validation-options.svg create mode 100644 docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage.svg create mode 100644 docsource/images/AxisIPCamera-entry-parameters-store-type-dialog.svg diff --git a/README.md b/README.md index 53c69fa..fcf114c 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,10 @@ 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) ##### Entry Parameters Tab @@ -226,15 +208,11 @@ 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)
@@ -243,12 +221,12 @@ 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 | | --------- | ----------- | ----------- | ----------- | | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | - | `11.6` _and_ newer | `net8.0` | | `net8.0` | + | `11.6` _and_ newer | `net8.0` | | `net8.0` | Unzip the archive containing extension assemblies to a known location. @@ -270,17 +248,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 @@ -319,11 +294,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 @@ -338,8 +310,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. | @@ -351,8 +323,6 @@ These values must match or the session will be denied. - - #### Using kfutil CLI
Click to expand details @@ -385,7 +355,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 @@ -401,10 +370,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\`. @@ -449,7 +416,6 @@ There are five (5) possible options: > * DNS = CN set in the Subject DN - ## Caveats > [!NOTE] @@ -459,11 +425,10 @@ There are five (5) possible options: > If a new alias is supplied, a Reenrollment job will not remove the original certificate associated with the \`Certificate Usage\`. > Since the camera has limited storage, it will be up to the user to remove any unused certificates via the AXIS Network Camera GUI. - ## License 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/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/scripts/store_types/bash/curl_create_store_types.sh b/scripts/store_types/bash/curl_create_store_types.sh index 3ab9e04..97f4d97 100755 --- a/scripts/store_types/bash/curl_create_store_types.sh +++ b/scripts/store_types/bash/curl_create_store_types.sh @@ -1,78 +1,20 @@ -#!/usr/bin/env bash +#!/bin/bash +# Store Type creation script using curl +# Generated by Doctool -# Creates all 1 store types via the Keyfactor Command REST API using curl. -# -# Authentication (first matching method is used): -# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN -# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET -# + KEYFACTOR_AUTH_TOKEN_URL -# Basic auth (AD): KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN -# -# Always required: -# KEYFACTOR_HOSTNAME Command hostname (e.g. my-command.example.com) -# -# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. +set -e -if [ -z "${KEYFACTOR_HOSTNAME}" ]; then - echo "ERROR: KEYFACTOR_HOSTNAME is required" - exit 1 -fi +# Configuration - set these variables before running +KEYFACTOR_HOSTNAME="${KEYFACTOR_HOSTNAME}" +KEYFACTOR_API_PATH="${KEYFACTOR_API_PATH:-KeyfactorAPI}" +KEYFACTOR_AUTH_TOKEN="${KEYFACTOR_AUTH_TOKEN}" -BASE_URL="https://${KEYFACTOR_HOSTNAME}/keyfactorapi" - -# --------------------------------------------------------------------------- -# Resolve auth -# --------------------------------------------------------------------------- -if [ -n "${KEYFACTOR_AUTH_ACCESS_TOKEN}" ]; then - BEARER_TOKEN="${KEYFACTOR_AUTH_ACCESS_TOKEN}" -elif [ -n "${KEYFACTOR_AUTH_CLIENT_ID}" ] && [ -n "${KEYFACTOR_AUTH_CLIENT_SECRET}" ] && [ -n "${KEYFACTOR_AUTH_TOKEN_URL}" ]; then - echo "Fetching OAuth token..." - BEARER_TOKEN=$(curl -s -X POST "${KEYFACTOR_AUTH_TOKEN_URL}" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "grant_type=client_credentials" \ - --data-urlencode "client_id=${KEYFACTOR_AUTH_CLIENT_ID}" \ - --data-urlencode "client_secret=${KEYFACTOR_AUTH_CLIENT_SECRET}" | jq -r '.access_token') - if [ -z "${BEARER_TOKEN}" ] || [ "${BEARER_TOKEN}" = "null" ]; then - echo "ERROR: Failed to fetch OAuth token from ${KEYFACTOR_AUTH_TOKEN_URL}" - exit 1 - fi -elif [ -n "${KEYFACTOR_USERNAME}" ] && [ -n "${KEYFACTOR_PASSWORD}" ] && [ -n "${KEYFACTOR_DOMAIN}" ]; then - BEARER_TOKEN="" -else - echo "ERROR: Authentication required. Set one of:" - echo " KEYFACTOR_AUTH_ACCESS_TOKEN" - echo " KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET + KEYFACTOR_AUTH_TOKEN_URL" - echo " KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN" - exit 1 -fi - -if [ -n "${BEARER_TOKEN}" ]; then - CURL_AUTH=("-H" "Authorization: Bearer ${BEARER_TOKEN}") -else - CURL_AUTH=("-u" "${KEYFACTOR_USERNAME}@${KEYFACTOR_DOMAIN}:${KEYFACTOR_PASSWORD}") -fi - -create_store_type() { - local name="$1" - local body="$2" - echo "Creating ${name} store type..." - response=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST "${BASE_URL}/certificatestoretypes" \ - -H "Content-Type: application/json" \ - -H "x-keyfactor-requested-with: APIClient" \ - "${CURL_AUTH[@]}" \ - -d "${body}") - if [ "$response" = "200" ] || [ "$response" = "201" ]; then - echo " OK (HTTP ${response})" - else - echo " FAILED (HTTP ${response})" - fi -} - -# --------------------------------------------------------------------------- -# AxisIPCamera — The IP address of the Camera. Sample is "192.167.231.174:44444". Include the port if necessary. -# --------------------------------------------------------------------------- -create_store_type "AxisIPCamera" '{ +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", @@ -83,7 +25,6 @@ create_store_type "AxisIPCamera" '{ "PrivateKeyAllowed": "Forbidden", "SupportedOperations": { "Add": true, - "Create": false, "Discovery": false, "Enrollment": true, "Remove": true @@ -94,13 +35,32 @@ create_store_type "AxisIPCamera" '{ "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 + "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": [ @@ -118,11 +78,8 @@ create_store_type "AxisIPCamera" '{ "Description": "The Certificate Usage to assign to the cert after enrollment. Can be left 'Other' to be assigned later." } ], - "StorePathDescription": "Enter the Serial Number of the camera e.g. `0b7c3d2f9e8a`", "StorePathType": "", "StorePathValue": "", "JobProperties": [] }' - -echo "Completed." diff --git a/scripts/store_types/bash/kfutil_create_store_types.sh b/scripts/store_types/bash/kfutil_create_store_types.sh index 27dfa46..aecc07d 100755 --- a/scripts/store_types/bash/kfutil_create_store_types.sh +++ b/scripts/store_types/bash/kfutil_create_store_types.sh @@ -1,28 +1,9 @@ -#!/usr/bin/env bash +#!/bin/bash +# Store Type creation script using kfutil +# Generated by Doctool -# Creates all 1 store types using kfutil. -# kfutil reads definitions from the Keyfactor integration catalog. -# -# Auth environment variables (first matching method is used): -# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN -# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET -# + KEYFACTOR_AUTH_TOKEN_URL -# Basic auth (AD): KEYFACTOR_HOSTNAME + KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD -# + KEYFACTOR_DOMAIN -# -# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. +set -e -if ! command -v kfutil &> /dev/null; then - echo "kfutil could not be found. Please install kfutil" - echo "See https://github.com/Keyfactor/kfutil#quickstart" - exit 1 -fi +echo "Creating store type: AxisIPCamera" +kfutil store-types create AxisIPCamera -if [ -z "$KEYFACTOR_HOSTNAME" ]; then - echo "KEYFACTOR_HOSTNAME not set — launching kfutil login" - kfutil login -fi - -kfutil store-types create --name "AxisIPCamera" - -echo "Done. All store types created." diff --git a/scripts/store_types/powershell/kfutil_create_store_types.ps1 b/scripts/store_types/powershell/kfutil_create_store_types.ps1 index 7280dd5..59f0203 100644 --- a/scripts/store_types/powershell/kfutil_create_store_types.ps1 +++ b/scripts/store_types/powershell/kfutil_create_store_types.ps1 @@ -1,29 +1,6 @@ -# Creates all 1 store types using kfutil. -# kfutil reads definitions from the Keyfactor integration catalog. -# -# Auth environment variables (first matching method is used): -# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN -# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET -# + KEYFACTOR_AUTH_TOKEN_URL -# Basic auth (AD): KEYFACTOR_HOSTNAME + KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD -# + KEYFACTOR_DOMAIN -# -# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. +# Store Type creation script using kfutil +# Generated by Doctool -# Uncomment if kfutil is not in your PATH -# Set-Alias -Name kfutil -Value 'C:\Program Files\Keyfactor\kfutil\kfutil.exe' +Write-Host "Creating store type: AxisIPCamera" +kfutil store-types create AxisIPCamera -if ($null -eq (Get-Command "kfutil" -ErrorAction SilentlyContinue)) { - Write-Host "kfutil could not be found. Please install kfutil" - Write-Host "See https://github.com/Keyfactor/kfutil#quickstart" - exit 1 -} - -if (-not $env:KEYFACTOR_HOSTNAME) { - Write-Host "KEYFACTOR_HOSTNAME not set — launching kfutil login" - & kfutil login -} - -& kfutil store-types create --name "AxisIPCamera" - -Write-Host "Done. All store types created." diff --git a/scripts/store_types/powershell/restmethod_create_store_types.ps1 b/scripts/store_types/powershell/restmethod_create_store_types.ps1 index e23ce6f..1c90eed 100644 --- a/scripts/store_types/powershell/restmethod_create_store_types.ps1 +++ b/scripts/store_types/powershell/restmethod_create_store_types.ps1 @@ -1,70 +1,19 @@ -# Creates all 1 store types via the Keyfactor Command REST API -# using PowerShell Invoke-RestMethod. -# -# Authentication (first matching method is used): -# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN -# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET -# + KEYFACTOR_AUTH_TOKEN_URL -# Basic auth (AD): KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN -# -# Always required: -# KEYFACTOR_HOSTNAME Command hostname (e.g. my-command.example.com) -# -# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. +# Store Type creation script using Invoke-RestMethod +# Generated by Doctool -if (-not $env:KEYFACTOR_HOSTNAME) { - Write-Error "KEYFACTOR_HOSTNAME is required" - exit 1 -} - -$uri = "https://$($env:KEYFACTOR_HOSTNAME)/keyfactorapi/certificatestoretypes" -$headers = @{ - 'Content-Type' = "application/json" - 'x-keyfactor-requested-with' = "APIClient" -} +# 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 -# --------------------------------------------------------------------------- -# Resolve auth -# --------------------------------------------------------------------------- -if ($env:KEYFACTOR_AUTH_ACCESS_TOKEN) { - $headers['Authorization'] = "Bearer $($env:KEYFACTOR_AUTH_ACCESS_TOKEN)" -} elseif ($env:KEYFACTOR_AUTH_CLIENT_ID -and $env:KEYFACTOR_AUTH_CLIENT_SECRET -and $env:KEYFACTOR_AUTH_TOKEN_URL) { - Write-Host "Fetching OAuth token..." - $tokenBody = @{ - grant_type = 'client_credentials' - client_id = $env:KEYFACTOR_AUTH_CLIENT_ID - client_secret = $env:KEYFACTOR_AUTH_CLIENT_SECRET - } - $tokenResp = Invoke-RestMethod -Method Post -Uri $env:KEYFACTOR_AUTH_TOKEN_URL -Body $tokenBody - $headers['Authorization'] = "Bearer $($tokenResp.access_token)" -} elseif ($env:KEYFACTOR_USERNAME -and $env:KEYFACTOR_PASSWORD -and $env:KEYFACTOR_DOMAIN) { - $cred = [System.Convert]::ToBase64String( - [System.Text.Encoding]::ASCII.GetBytes( - "$($env:KEYFACTOR_USERNAME)@$($env:KEYFACTOR_DOMAIN):$($env:KEYFACTOR_PASSWORD)")) - $headers['Authorization'] = "Basic $cred" -} else { - Write-Error ("Authentication required. Set one of:`n" + - " KEYFACTOR_AUTH_ACCESS_TOKEN`n" + - " KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET + KEYFACTOR_AUTH_TOKEN_URL`n" + - " KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN") - exit 1 +$Headers = @{ + "Authorization" = "Bearer $KeyfactorAuthToken" + "Content-Type" = "application/json" + "x-keyfactor-requested-with" = "APIClient" } -function New-StoreType { - param([string]$Name, [string]$Body) - Write-Host "Creating $Name store type..." - try { - Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -Body $Body -ContentType "application/json" | Out-Null - Write-Host " OK" - } catch { - Write-Warning " FAILED: $($_.Exception.Message)" - } -} - -# --------------------------------------------------------------------------- -# AxisIPCamera — The IP address of the Camera. Sample is "192.167.231.174:44444". Include the port if necessary. -# --------------------------------------------------------------------------- -New-StoreType "AxisIPCamera" @' +Write-Host "Creating store type: AxisIPCamera" +$Body = @' { "Name": "Axis IP Camera", "ShortName": "AxisIPCamera", @@ -76,7 +25,6 @@ New-StoreType "AxisIPCamera" @' "PrivateKeyAllowed": "Forbidden", "SupportedOperations": { "Add": true, - "Create": false, "Discovery": false, "Enrollment": true, "Remove": true @@ -87,13 +35,32 @@ New-StoreType "AxisIPCamera" @' "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 + "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": [ @@ -111,12 +78,11 @@ New-StoreType "AxisIPCamera" @' "Description": "The Certificate Usage to assign to the cert after enrollment. Can be left 'Other' to be assigned later." } ], - "StorePathDescription": "Enter the Serial Number of the camera e.g. `0b7c3d2f9e8a`", "StorePathType": "", "StorePathValue": "", "JobProperties": [] } '@ +Invoke-RestMethod -Uri "https://$KeyfactorHostname/$KeyfactorApiPath/CertificateStoreTypes" -Method POST -Headers $Headers -Body $Body -Write-Host "Completed." From 639090558159616de1642f0ed631fd6fb18494e2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 2 Jul 2026 20:29:21 +0000 Subject: [PATCH 08/10] docs: auto-generate README and documentation [skip ci] --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index fcf114c..768e8c3 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,7 @@ the Keyfactor Command Portal 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.svg) + ![AxisIPCamera Custom Field - ServerUseSsl](docsource/images/AxisIPCamera-custom-field-ServerUseSsl-validation-options-dialog.svg) ##### Entry Parameters Tab @@ -213,6 +214,7 @@ the Keyfactor Command Portal 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.svg) + ![AxisIPCamera Entry Parameter - CertUsage](docsource/images/AxisIPCamera-entry-parameters-store-type-dialog-CertUsage-validation-options.svg) From f51f97dc7de59929eb821d3a112128b85ef864ae Mon Sep 17 00:00:00 2001 From: Elena Fiocca Date: Thu, 2 Jul 2026 15:24:39 -0600 Subject: [PATCH 09/10] Updated README --- docsource/axisipcamera.md | 20 +------------- docsource/content.md | 58 ++++++++++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/docsource/axisipcamera.md b/docsource/axisipcamera.md index 1971ad6..8e048bc 100644 --- a/docsource/axisipcamera.md +++ b/docsource/axisipcamera.md @@ -36,22 +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. -> These CA certificates establish trust for TLS connections initiated by the camera. - -> [!NOTE] -> As of Keyfactor Command v25.4, SANs can be provided for a Reenrollment (ODKG) job. -> You must also have installed, at minimum, the Keyfactor Universal Orchestator v25.1 -> in order for the SANs to be sent to the orchestrator. -> -> The Axis IP Camera API *only* supports the addition of DNS and IP SANs. If you add other SAN types to the ODKG job, these will be ignored and not added to the certificate. -> -> * If SANs are NOT provided and the \`Certificate Usage\` assigned is **HTTPS**, IP and DNS will be automatically added as SANs to an enrolled certificate associated with a NEW alias. -> -> * 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 338b04e..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,11 +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 -> [!NOTE] -> v1.1.0 - A Reenrollment job will now replace the certificate contents for an existing alias on the camera, therefore, not requiring -> a new alias be supplied for every new certificate enrollment. -> -> If a new alias is supplied, a Reenrollment job will not remove the original certificate associated with the \`Certificate Usage\`. -> Since the camera has limited storage, it will be up to the user to remove any unused certificates via the AXIS Network Camera GUI. +### 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. + From 40e546a304da2a4584009266d4451385004ee7bb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 2 Jul 2026 21:26:01 +0000 Subject: [PATCH 10/10] docs: auto-generate README and documentation [skip ci] --- README.md | 64 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 768e8c3..1562bcd 100644 --- a/README.md +++ b/README.md @@ -399,33 +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. -> These CA certificates establish trust for TLS connections initiated by the camera. + +## 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] -> As of Keyfactor Command v25.4, SANs can be provided for a Reenrollment (ODKG) job. -> You must also have installed, at minimum, the Keyfactor Universal Orchestator v25.1 -> in order for the SANs to be sent to the orchestrator. -> -> The Axis IP Camera API *only* supports the addition of DNS and IP SANs. If you add other SAN types to the ODKG job, these will be ignored and not added to the certificate. -> -> * If SANs are NOT provided and the \`Certificate Usage\` assigned is **HTTPS**, IP and DNS will be automatically added as SANs to an enrolled certificate associated with a NEW alias. +> 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. > -> * IP = Client Machine configured for the certificate store (excluding any port) -> -> * DNS = CN set in the Subject DN +> 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] -> v1.1.0 - A Reenrollment job will now replace the certificate contents for an existing alias on the camera, therefore, not requiring -> a new alias be supplied for every new certificate enrollment. +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: > -> If a new alias is supplied, a Reenrollment job will not remove the original certificate associated with the \`Certificate Usage\`. -> Since the camera has limited storage, it will be up to the user to remove any unused certificates via the AXIS Network Camera GUI. +> - **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