Skip to content

Commit 984ab17

Browse files
committed
Save local changes
1 parent c5d24a8 commit 984ab17

7 files changed

Lines changed: 229 additions & 76 deletions

File tree

AxisIPCamera/AxisIPCamera.csproj

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,54 @@
22

33
<PropertyGroup>
44
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
5-
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
5+
<TargetFramework>net8.0</TargetFramework>
66
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
77
<ImplicitUsings>disable</ImplicitUsings>
88
<RootNamespace>Keyfactor.Extensions.Orchestrator.AxisIPCamera</RootNamespace>
9+
<FileVersion>1.1.0</FileVersion>
910
</PropertyGroup>
1011

1112
<ItemGroup>
12-
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" />
13-
<PackageReference Include="Keyfactor.Logging" Version="1.1.1" />
14-
15-
<None Update="manifest.json">
16-
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
17-
</None>
18-
19-
<PackageReference Include="Keyfactor.Orchestrators.IOrchestratorJobExtensions" Version="1.0.0" />
20-
21-
<PackageReference Include="RestSharp" Version="112.1.0" />
22-
23-
<None Update="Files\SetHttpsBinding.xml">
24-
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
25-
</None>
26-
27-
<None Update="Files\SetIEEEBinding.xml">
28-
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
29-
</None>
30-
31-
<None Update="Files\SetMQTTBinding.json">
32-
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
33-
</None>
34-
35-
<None Update="Files\GetHttpsBinding.xml">
36-
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
37-
</None>
38-
39-
<None Update="Files\GetIEEEBinding.xml">
40-
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
41-
</None>
42-
43-
<None Update="Files\GetMQTTBinding.json">
44-
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
45-
</None>
13+
14+
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.1" />
15+
16+
<PackageReference Include="Keyfactor.Logging" Version="1.3.0" />
17+
18+
<PackageReference Include="Keyfactor.Orchestrators.IOrchestratorJobExtensions" Version="1.0.0" />
19+
20+
<PackageReference Include="Keyfactor.PKI" Version="8.3.1" />
21+
22+
<PackageReference Include="RestSharp" Version="112.1.0" />
23+
24+
<PackageReference Include="System.Formats.Asn1" Version="8.0.1" />
25+
26+
<None Update="manifest.json">
27+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
28+
</None>
29+
30+
<None Update="Files\SetHttpsBinding.xml">
31+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
32+
</None>
33+
34+
<None Update="Files\SetIEEEBinding.xml">
35+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
36+
</None>
37+
38+
<None Update="Files\SetMQTTBinding.json">
39+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
40+
</None>
41+
42+
<None Update="Files\GetHttpsBinding.xml">
43+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
44+
</None>
45+
46+
<None Update="Files\GetIEEEBinding.xml">
47+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
48+
</None>
49+
50+
<None Update="Files\GetMQTTBinding.json">
51+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
52+
</None>
4653
</ItemGroup>
4754

4855
</Project>

AxisIPCamera/Client/AxisHttpClient.cs

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
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.Collections.Generic;
910
using System.Reflection;
1011
using System.IO;
1112
using System.Linq;
@@ -285,8 +286,7 @@ public Constants.Keystore GetDefaultKeystore()
285286
/// <param name="keyType">Combination of key algorithm and key size</param>
286287
/// <param name="keystore">Default keystore for the device</param>
287288
/// <param name="subject">Subject provided for the certificate</param>
288-
/// <param name="sans">Subject Alternative Names</param>
289-
public void CreateSelfSignedCert(string alias, string keyType, string keystore, string subject, string[] sans)
289+
public void CreateSelfSignedCert(string alias, string keyType, string keystore, string subject)
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 = sans,
306+
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,25 +347,45 @@ public void CreateSelfSignedCert(string alias, string keyType, string keystore,
347347
}
348348

349349
/// <summary>
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.
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.
352353
/// </summary>
353354
/// <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>
354357
/// <returns>CSR string</returns>
355-
public string ObtainCSR(string alias)
358+
public string ObtainCSR(string alias, string subject, List<string> sans)
356359
{
357360
try
358361
{
359362
Logger.MethodEntry();
360363

361364
var postCSRResource = $"{Constants.RestApiEntryPoint}/certificates/{alias}/get_csr";
362365

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.
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.
365368
// If there are attributes assigned by the CA, those will override the attributes that end up
366369
// in the certificate signed by the CA.
367-
string jsonBody = @"{""data"":{}}";
368-
var httpResponse = ExecuteHttp(postCSRResource, Method.Post, Constants.ApiType.Rest, jsonBody);
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());
369389

370390
// Decode the HTTP response if failed
371391
if (httpResponse is {IsSuccessful:false})

AxisIPCamera/Helpers/DeviceCertValidator.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ public static class DeviceCertValidator
7373

7474
// Add TLS cert as leaf certificate to the end of the custom chain
7575
customChain.Add(parser.ReadCertificate(cert.RawData));
76+
77+
if (!File.Exists(trustedIntCertPath))
78+
{
79+
logger.LogError($"{trustedIntCertPath} does not exist.");
80+
return false;
81+
}
7682

7783
logger.LogTrace($"Loading Trusted Intermediate Certs from {trustedIntCertPath}");
7884
var trustedIntCerts = parser.ReadCertificates(File.ReadAllBytes(trustedIntCertPath));
@@ -92,6 +98,12 @@ public static class DeviceCertValidator
9298

9399
logger.LogTrace($"{trustedIntCerts.Count} Trusted Intermediate Certs found");
94100

101+
if (!File.Exists(trustedRootCertPath))
102+
{
103+
logger.LogError($"{trustedRootCertPath} does not exist.");
104+
return false;
105+
}
106+
95107
logger.LogTrace($"Loading Trusted Root Cert from {trustedRootCertPath}");
96108
var trustedRootCerts = parser.ReadCertificates(File.ReadAllBytes(trustedRootCertPath));
97109

@@ -214,8 +226,8 @@ private static bool VerifyAkiSkiChain(List<X509Certificate> customChain, ILogger
214226
{
215227
logger.MethodEntry();
216228

217-
logger.LogTrace("Custom chain being validated includes: (1) Leaf cert from TLS session, (2) n-Intermediate certs from custom trust, &" +
218-
"n-Root certs from custom trust");
229+
logger.LogTrace("Custom chain being validated includes: (1) Leaf cert from TLS session, (2) n-Intermediate certs from custom trust, & " +
230+
"(3) n-Root certs from custom trust");
219231

220232
for (int i = 0; i < customChain.Count - 1; i++)
221233
{

AxisIPCamera/Helpers/SANBuilder.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2026 Keyfactor
2+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
3+
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4+
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
5+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
6+
// and limitations under the License.
7+
8+
using System.Collections.Generic;
9+
using System.Linq;
10+
using Microsoft.Extensions.Logging;
11+
12+
namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Helpers
13+
{
14+
public static class SANBuilder
15+
{
16+
public static List<string> BuildSANString(Dictionary<string, string[]> sans, ILogger logger)
17+
{
18+
var parts = new List<string>();
19+
20+
if (sans == null || sans.Count == 0)
21+
{
22+
logger.LogTrace($"SANs is null or empty");
23+
return parts;
24+
}
25+
26+
foreach (var entry in sans)
27+
{
28+
string key = NormalizeSanKey(entry.Key);
29+
30+
// The Axis API only supports the addition of 'dns' and 'ip' SAN type keys
31+
if (key is not ("DNS" or "IP"))
32+
continue;
33+
34+
if (entry.Value == null || entry.Value.Length == 0)
35+
continue;
36+
37+
// NOTE: We are separating the key and value pairs with a colon because this is the format
38+
// required to send SANs to the Axis API endpoint
39+
parts.AddRange(
40+
entry.Value
41+
.Where(v => !string.IsNullOrWhiteSpace(v))
42+
.Select(v => $@"""{key}:{v.Trim()}""")
43+
);
44+
}
45+
46+
return parts;
47+
}
48+
49+
/// <summary>
50+
/// Normalize SAN type keys to RFC-compliant names.
51+
/// **NOTE: The Axis API only supports the addition of 'dns' and 'ip' SAN types.
52+
/// Courtesy of B.Pokorny.
53+
/// </summary>
54+
private static string NormalizeSanKey(string key)
55+
{
56+
return key.Trim().ToLower() switch
57+
{
58+
"dns" => "DNS",
59+
"ip" or "ip4" or "ip6" => "IP",
60+
_ => key.ToLower() // default
61+
};
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)