-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathManagement.cs
More file actions
277 lines (236 loc) · 16.1 KB
/
Management.cs
File metadata and controls
277 lines (236 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
// Copyright 2025 Keyfactor
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
// and limitations under the License.
using System;
using System.Text;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Keyfactor.Logging;
using Keyfactor.Orchestrators.Extensions;
using Keyfactor.Orchestrators.Common.Enums;
using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Client;
using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Model;
using Keyfactor.Orchestrators.Extensions.Interfaces;
namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera
{
public class Management : IManagementJobExtension
{
private readonly ILogger _logger;
//Necessary to implement IManagementJobExtension but not used. Leave as empty string.
public string ExtensionName => "";
public IPAMSecretResolver Resolver;
public Management(IPAMSecretResolver resolver)
{
_logger = LogHandler.GetClassLogger<Management>();
Resolver = resolver;
}
//Job Entry Point
public JobResult ProcessJob(ManagementJobConfiguration config)
{
//METHOD ARGUMENTS...
//config - contains context information passed from KF Command to this job run:
//
// config.Server.Username, config.Server.Password - credentials for orchestrated server - use to authenticate to certificate store server.
//
// config.ServerUsername, config.ServerPassword - credentials for orchestrated server - use to authenticate to certificate store server.
// config.CertificateStoreDetails.ClientMachine - server name or IP address of orchestrated server
// config.CertificateStoreDetails.StorePath - location path of certificate store on orchestrated server
// config.CertificateStoreDetails.StorePassword - if the certificate store has a password, it would be passed here
// config.CertificateStoreDetails.Properties - JSON string containing custom store properties for this specific store type
//
// config.JobCertificate.EntryContents - Base64 encoded string representation (PKCS12 if private key is included, DER if not) of the certificate to add for Management-Add jobs.
// config.JobCertificate.Alias - optional string value of certificate alias (used in java keystores and some other store types)
// config.OperationType - enumeration representing function with job type. Used only with Management jobs where this value determines whether the Management job is a CREATE/ADD/REMOVE job.
// config.Overwrite - Boolean value telling the Orchestrator Extension whether to overwrite an existing certificate in a store. How you determine whether a certificate is "the same" as the one provided is AnyAgent implementation dependent
// config.JobCertificate.PrivateKeyPassword - For a Management Add job, if the certificate being added includes the private key (therefore, a pfx is passed in config.JobCertificate.EntryContents), this will be the password for the pfx.
//NLog Logging to c:\CMS\Logs\CMS_Agent_Log.txt
try
{
_logger.MethodEntry();
_logger.LogTrace($"Begin Management for Client Machine {config.CertificateStoreDetails.ClientMachine}...");
//Management jobs, unlike Discovery, Inventory, and Reenrollment jobs can have 3 different purposes:
switch (config.OperationType)
{
case CertStoreOperationType.Add:
{
_logger.LogInformation("Entered Management-Add Operation");
//OperationType == Add - Add a certificate to the certificate store passed in the config object
//Code logic to:
// 1) Connect to the orchestrated server (config.CertificateStoreDetails.ClientMachine) containing the certificate store
// 2) Custom logic to add certificate to certificate store (config.CertificateStoreDetails.StorePath) possibly using alias as an identifier if applicable (config.JobCertificate.Alias). Use alias and overwrite flag (config.Overwrite)
// to determine if job should overwrite an existing certificate in the store, for example a renewal.
// Retrieve management config from Command
string jsonConfig = JsonConvert.SerializeObject(config);
_logger.LogDebug($"Management Config: {jsonConfig.Replace(config.ServerPassword,"**********")}");
_logger.LogDebug($"Client Machine: {config.CertificateStoreDetails.ClientMachine}");
// Get needed information from config
string alias = config.JobCertificate.Alias;
bool overwrite = config.Overwrite;
string certBase64Der = config.JobCertificate.Contents;
_logger.LogDebug($"Certificate contents:{certBase64Der}");
// Prevent add of client certs; Client certs may only be added via reenrollment
if (IsCACertificate(certBase64Der))
{
_logger.LogInformation("Certificate is a CA trust cert. Proceeding with Add operation...");
}
else
{
_logger.LogWarning("Certificate is an end-entity cert. Unable to add this certificate type to a device.");
return new JobResult()
{
Result = OrchestratorJobStatusJobResult.Warning,
JobHistoryId = config.JobHistoryId,
FailureMessage = $"UNSUPPORTED OPERATION --- This certificate cannot be used as a Trust. Unable to add end-entity certificates to a device."
};
}
// Create client to connect to device
_logger.LogTrace("Creating Api HTTP Client...");
var client = new AxisHttpClient(config, config.CertificateStoreDetails, Resolver);
_logger.LogTrace("Api HTTP Client Created...");
// Ignore the 'Overwrite' flag; Currently NOT supporting overwriting an existing CA cert with the same alias.
// The existing CA cert needs to be deleted first and then the new CA cert can be added with the same alias.
// Log warning if user attempts to add a CA cert with the same alias.
_logger.LogInformation($"Overwrite flag = {overwrite} --- IGNORING");
// Perform CA cert inventory
_logger.LogTrace("Retrieve all CA certificates");
CACertificateData data1 = client.ListCACertificates();
// Look for a CA cert with the same alias as the one requested
_logger.LogTrace($"Searching for an existing CA cert with alias '{alias}'...");
var existingCACert = data1.CACerts.FirstOrDefault(c => c.Alias == alias);
if (null == existingCACert)
{
_logger.LogInformation($"Alias '{alias}' does not exist for any CA certificates. Proceeding with Add operation...");
}
else
{
_logger.LogWarning($"A CA certificate was found with the alias '{alias}'. Unable to add this certificate.");
return new JobResult()
{
Result = OrchestratorJobStatusJobResult.Warning,
JobHistoryId = config.JobHistoryId,
FailureMessage = $"ALIAS ALREADY EXISTS FOR CA CERTIFICATE --- Provide a new alias and resubmit."
};
}
// Build PEM content
string formattedDer = InsertLineBreaks(certBase64Der, 64);
_logger.LogDebug(($"Formatted certificate contents:\n{formattedDer}"));
StringBuilder pemBuilder = new StringBuilder();
pemBuilder.Append(@"-----BEGIN CERTIFICATE-----\n");
var noLineBreaks = formattedDer.Replace("\n", @"\n");
pemBuilder.Append(noLineBreaks);
pemBuilder.Append(@"\n-----END CERTIFICATE-----");
var pemCert = pemBuilder.ToString();
// Add certificate with alias to the device
client.AddCACertificate(alias, pemCert);
break;
}
case CertStoreOperationType.Remove:
{
_logger.LogInformation("Entered Management-Remove Operation");
//OperationType == Remove - Delete a certificate from the certificate store passed in the config object
//Code logic to:
// 1) Connect to the orchestrated server (config.CertificateStoreDetails.ClientMachine) containing the certificate store
// 2) Custom logic to remove the certificate in a certificate store (config.CertificateStoreDetails.StorePath), possibly using alias (config.JobCertificate.Alias) or certificate thumbprint to identify the certificate (implementation dependent)
// Retrieve management config from Command
_logger.LogDebug($"Management Config {JsonConvert.SerializeObject(config)}");
_logger.LogDebug($"Client Machine: {config.CertificateStoreDetails.ClientMachine}");
// Get needed information from config
string alias = config.JobCertificate.Alias;
string certBase64Der = config.JobCertificate.Contents;
// Prevent removal of client certs; Client certs may be removed as part of a future update
if (IsCACertificate(certBase64Der))
{
_logger.LogInformation("Certificate is a CA trust cert. Proceeding with Remove operation...");
}
else
{
_logger.LogWarning("Certificate is an end-entity cert. Unable to remove this certificate type from a device.");
return new JobResult()
{
Result = OrchestratorJobStatusJobResult.Warning,
JobHistoryId = config.JobHistoryId,
FailureMessage = $"UNSUPPORTED OPERATION --- This certificate is an end-entity cert. Unable to remove end-entity certificates from a device."
};
}
// Create client to connect to device
_logger.LogTrace("Creating Api HTTP Client...");
var client = new AxisHttpClient(config, config.CertificateStoreDetails, Resolver);
_logger.LogTrace("Api HTTP Client Created...");
// Remove certificate with alias from the device
client.RemoveCACertificate(alias);
break;
}
default:
//Invalid OperationType. Return error. Should never happen though
return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}: Unsupported operation: {config.OperationType.ToString()}" };
}
}
catch (Exception ex)
{
//Status: 2=Success, 3=Warning, 4=Error
return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = $"Management Job Failed During '{config.OperationType.ToString()}' Operation: {ex.Message} - Refer to logs for more detailed information." };
}
//Status: 2=Success, 3=Warning, 4=Error
return new JobResult() { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId };
}
/// <summary>
/// Inserts line breaks every n-characters.
/// </summary>
/// <param name="input">String to break apart every n-lines</param>
/// <param name="lineLength">Length of each line</param>
/// <returns>Formatted string</returns>
private static string InsertLineBreaks(string input, int lineLength)
{
int length = input.Length;
int lines = (length + lineLength - 1) / lineLength; // Calculate the number of lines needed
char[] result = new char[length + lines - 1]; // Extra space for new line characters
int inputIndex = 0;
int resultIndex = 0;
for (int i = 0; i < lines; i++)
{
int remainingChars = Math.Min(lineLength, length - inputIndex);
input.Substring(inputIndex, remainingChars).CopyTo(0, result, resultIndex, remainingChars);
inputIndex += remainingChars;
resultIndex += remainingChars;
// Add a new line after every 64 characters except for the last line
if (i < lines - 1)
{
result[resultIndex++] = '\n';
}
}
return new string(result);
}
/// <summary>
/// ASSUMPTION: This function assumes a certificate to be an end entity certificate
/// if the basic constraints extension is NOT present in a version 3 certificate.
/// If the basic constraints extension IS present, it must have a value marked for 'CertificateAuthority'.
/// FALLBACK: If this check produces a false positive, the Axis API will fail on the
/// HTTP request and details will be logged accordingly.
/// </summary>
/// <param name="certBase64Der">Cert contents to add represented as Base-64 encoded DER</param>
/// <returns>True if CA cert; False otherwise</returns>
private static bool IsCACertificate(string certBase64Der)
{
// Convert the cert contents to a byte array
byte[] certBytes = Convert.FromBase64String(certBase64Der);
// Create an X509 object so we can analyze the contents
X509Certificate2 certToAdd = new X509Certificate2(certBytes);
foreach (X509Extension ext in certToAdd.Extensions)
{
if (ext.Oid?.Value == "2.5.29.19") // Indicates if the subject may act as a CA, with public key used to verify cert signatures
{
if (ext is X509BasicConstraintsExtension basicConstraints)
{
return basicConstraints.CertificateAuthority;
}
}
}
return false;
}
}
}