Skip to content

Commit 8c632ce

Browse files
committed
Save local changes
1 parent 984ab17 commit 8c632ce

4 files changed

Lines changed: 160 additions & 93 deletions

File tree

AxisIPCamera/Client/AxisHttpClient.cs

Lines changed: 72 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
// and limitations under the License.
77

88
using System;
9-
using System.Collections.Generic;
109
using System.Reflection;
1110
using System.IO;
1211
using System.Linq;
@@ -286,7 +285,8 @@ public Constants.Keystore GetDefaultKeystore()
286285
/// <param name="keyType">Combination of key algorithm and key size</param>
287286
/// <param name="keystore">Default keystore for the device</param>
288287
/// <param name="subject">Subject provided for the certificate</param>
289-
public void CreateSelfSignedCert(string alias, string keyType, string keystore, string subject)
288+
/// <param name="sans">Subject Alternative Names</param>
289+
public void CreateSelfSignedCert(string alias, string keyType, string keystore, string subject, string[] sans)
290290
{
291291
try
292292
{
@@ -303,7 +303,7 @@ public void CreateSelfSignedCert(string alias, string keyType, string keystore,
303303
KeyType = keyType,
304304
Keystore = keystore,
305305
Subject = subject,
306-
SANS = [],
306+
SANS = sans,
307307
ValidFrom = 0, // Cert validity period will be determined by the template
308308
ValidTo = 0 // Cert validity period will be determined by the template
309309
}
@@ -347,45 +347,25 @@ public void CreateSelfSignedCert(string alias, string keyType, string keystore,
347347
}
348348

349349
/// <summary>
350-
/// Obtains a CSR for the self-signed or existing certificate with private key on the device.
351-
/// Fields from the certificate will be copied into the CSR.
352-
/// SANs will be added to the CSR.
350+
/// Obtains a CSR for the self-signed certificate with private key on the device.
351+
/// Fields from the self-signed certificate will be copied into the CSR.
353352
/// </summary>
354353
/// <param name="alias">Unique identifier for the cert to be generated from the CSR</param>
355-
/// <param name="subject">Subject provided for the certificate</param>
356-
/// <param name="sans">Subject Alternative Names</param>
357354
/// <returns>CSR string</returns>
358-
public string ObtainCSR(string alias, string subject, List<string> sans)
355+
public string ObtainCSR(string alias)
359356
{
360357
try
361358
{
362359
Logger.MethodEntry();
363360

364361
var postCSRResource = $"{Constants.RestApiEntryPoint}/certificates/{alias}/get_csr";
365362

366-
// Compose the body --- This is required.
367-
// All information obtained in the self-signed or existing cert will be used to create the CSR.
363+
// Compose the body --- This is required, but leaving the contents blank.
364+
// All information obtained in the self-signed cert will be used to create the CSR.
368365
// If there are attributes assigned by the CA, those will override the attributes that end up
369366
// in the certificate signed by the CA.
370-
// 1. If a field is filled out, that value will be used in the CSR
371-
// 2. If a field is NOT filled out, the existing value from the existing certificate will be copied into the CSR
372-
// 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
373-
var jsonBody = new StringBuilder(@"{""data"":{");
374-
375-
if (sans.Count == 0)
376-
{
377-
jsonBody.Append(@"""subject"":""").Append(subject).Append("}}");
378-
}
379-
else
380-
{
381-
jsonBody.Append(@"""subject"":""").Append(subject).Append(@""",""subject_alt_names"": [");
382-
string result = string.Join(",", sans);
383-
jsonBody.Append(result).Append("]}}");
384-
}
385-
386-
Logger.LogDebug($"POST Request Body: {jsonBody}");
387-
388-
var httpResponse = ExecuteHttp(postCSRResource, Method.Post, Constants.ApiType.Rest, jsonBody.ToString());
367+
string jsonBody = @"{""data"":{}}";
368+
var httpResponse = ExecuteHttp(postCSRResource, Method.Post, Constants.ApiType.Rest, jsonBody);
389369

390370
// Decode the HTTP response if failed
391371
if (httpResponse is {IsSuccessful:false})
@@ -544,30 +524,83 @@ public void RemoveCACertificate(string alias)
544524
Logger.LogError($"HTTP Request unsuccessful - HTTP Response: {DecodeHttpStatus(httpResponse)}");
545525
throw new Exception($"HTTP Request unsuccessful.");
546526
}
527+
547528
// Decode the API response when HTTP response is successful
529+
if (httpResponse != null && string.IsNullOrEmpty(httpResponse.Content))
530+
{
531+
throw new Exception("No content returned from HTTP Response");
532+
}
533+
534+
RestApiResponse apiResponse = JsonConvert.DeserializeObject<RestApiResponse>(httpResponse.Content);
535+
if (apiResponse.Status == Constants.Status.Success)
536+
{
537+
Logger.MethodExit();
538+
}
548539
else
549540
{
550-
if (httpResponse != null && string.IsNullOrEmpty(httpResponse.Content))
551-
{
552-
throw new Exception("No content returned from HTTP Response");
553-
}
541+
ErrorData error = JsonConvert.DeserializeObject<ErrorData>(httpResponse.Content);
542+
throw new Exception(
543+
$"API error encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})");
544+
}
545+
}
546+
catch (Exception e)
547+
{
548+
Logger.LogError("Error completing CA certificate remove: " + LogHandler.FlattenException(e));
549+
throw new Exception(e.Message);
550+
}
551+
}
552+
553+
/// <summary>
554+
/// Removes a certificate with private key from the device.
555+
/// </summary>
556+
/// <param name="alias">Unique identifier of the CA certificate to be removed</param>
557+
public void RemoveCertificate(string alias)
558+
{
559+
try
560+
{
561+
Logger.MethodEntry();
554562

555-
RestApiResponse apiResponse = JsonConvert.DeserializeObject<RestApiResponse>(httpResponse.Content);
556-
if (apiResponse.Status == Constants.Status.Success)
563+
var deleteCertResource = $"{Constants.RestApiEntryPoint}/certificates/{alias}";
564+
var httpResponse = ExecuteHttp(deleteCertResource, Method.Delete);
565+
566+
// Decode the HTTP response if failed
567+
if (httpResponse is { IsSuccessful: false })
568+
{
569+
Logger.LogError($"HTTP Request unsuccessful - HTTP Response: {DecodeHttpStatus(httpResponse)}");
570+
throw new Exception($"HTTP Request unsuccessful.");
571+
}
572+
573+
// Decode the API response when HTTP response is successful
574+
if (httpResponse != null && string.IsNullOrEmpty(httpResponse.Content))
575+
{
576+
throw new Exception("No content returned from HTTP Response");
577+
}
578+
579+
RestApiResponse apiResponse = JsonConvert.DeserializeObject<RestApiResponse>(httpResponse.Content);
580+
if (apiResponse.Status == Constants.Status.Success)
581+
{
582+
Logger.MethodExit();
583+
}
584+
else
585+
{
586+
ErrorData error = JsonConvert.DeserializeObject<ErrorData>(httpResponse.Content);
587+
588+
// Check for error code 5 - "Validation error: Certificate is in use" or "Validation error: Certificate is not deletable" ---
589+
// This will capture all device ID certs, which we do not want to delete anyway
590+
if (error.ErrorInfo is { Code: 5, Message: "Validation error: Certificate is not deletable" or "Validation error: Certificate is in use"})
557591
{
558-
Logger.MethodExit();
592+
Logger.LogWarning($"API warning encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})");
559593
}
560594
else
561595
{
562-
ErrorData error = JsonConvert.DeserializeObject<ErrorData>(httpResponse.Content);
563596
throw new Exception(
564597
$"API error encountered - {error.ErrorInfo.Message} - (Code: {error.ErrorInfo.Code})");
565598
}
566599
}
567600
}
568601
catch (Exception e)
569602
{
570-
Logger.LogError("Error completing CA certificate remove: " + LogHandler.FlattenException(e));
603+
Logger.LogError("Error completing certificate remove: " + LogHandler.FlattenException(e));
571604
throw new Exception(e.Message);
572605
}
573606
}

AxisIPCamera/Helpers/SANBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Helpers
1313
{
1414
public static class SANBuilder
1515
{
16-
public static List<string> BuildSANString(Dictionary<string, string[]> sans, ILogger logger)
16+
public static List<string> BuildSANList(Dictionary<string, string[]> sans, ILogger logger)
1717
{
1818
var parts = new List<string>();
1919

@@ -39,7 +39,7 @@ public static List<string> BuildSANString(Dictionary<string, string[]> sans, ILo
3939
parts.AddRange(
4040
entry.Value
4141
.Where(v => !string.IsNullOrWhiteSpace(v))
42-
.Select(v => $@"""{key}:{v.Trim()}""")
42+
.Select(v => $"{key}:{v.Trim()}")
4343
);
4444
}
4545

AxisIPCamera/Model/Constants.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
// Copyright 2025 Keyfactor
1+
// Copyright 2026 Keyfactor
22
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
33
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
44
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
55
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
66
// and limitations under the License.
77

88
using System;
9+
using System.Globalization;
910
using System.IO;
11+
using System.Text.RegularExpressions;
1012
using Newtonsoft.Json;
1113
using Org.BouncyCastle.OpenSsl;
1214
using Org.BouncyCastle.Pkcs;
@@ -16,7 +18,7 @@ namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Model
1618
public static class Constants
1719
{
1820
// This is the API entry point for the REST VAPIX Cert Management API
19-
public static string RestApiEntryPoint = "/config/rest/cert/v1beta";
21+
public static string RestApiEntryPoint = "/config/rest/cert/v1";
2022

2123
// This is the API entry point for the SOAP Cert Management API
2224
public static string SoapApiEntryPoint = "/vapix/services";
@@ -248,4 +250,31 @@ public override void WriteJson(
248250
}
249251
}
250252
}
253+
254+
public static class CertificateName
255+
{
256+
/// <summary>
257+
/// Returns a UTC-based suffix, i.e. "2602171544"
258+
/// </summary>
259+
public static string GetUtcSuffix() =>
260+
DateTime.UtcNow.ToString("yyMMddHHmm", CultureInfo.InvariantCulture);
261+
262+
/// <summary>
263+
/// Creates a unique certificate name by appending ['_' + Utc DateTime suffix] to the end of the user-supplied certificate name.
264+
/// Example: "_2602171544"
265+
/// </summary>
266+
public static string CreateUniqueCertName(string certName)
267+
{
268+
// check to see if the old cert name had a previously appended timestamp
269+
// EDGE CASE: Scenario under which this could happen - Cert name bound to usage is known and used to schedule an ODKG job
270+
Regex rgx = new Regex(@"_[0-9]{10}$",RegexOptions.CultureInvariant);
271+
var m = Regex.Match(certName,@"_[0-9]{10}$");
272+
if (m.Success)
273+
{
274+
return certName.Remove(m.Index, m.Length) + "_" + GetUtcSuffix();
275+
}
276+
277+
return certName + "_" + GetUtcSuffix();
278+
}
279+
}
251280
}

0 commit comments

Comments
 (0)