This commit is contained in:
2025-10-18 21:43:09 +02:00
parent 43fba99802
commit 7dd395e186
17 changed files with 156 additions and 120 deletions

View File

@@ -117,23 +117,11 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
using (X509Certificate2 publicOnlyCert = request.Create(settings.Issuer.SubjectName, sgen, notBefore, notAfter, serial)) using (X509Certificate2 publicOnlyCert = request.Create(settings.Issuer.SubjectName, sgen, notBefore, notAfter, serial))
{ {
cert = JoinPrivateKey(publicOnlyCert, privateKey); cert = JoinPrivateKey(publicOnlyCert, privateKey);
// using (X509Certificate2 temp = JoinPrivateKey(publicOnlyCert, privateKey))
// {
// // Generated instance of the cert can't be added to cert-store (private key is missing) if requested by the caller.
// // To avoid this recreate the cert
// cert = Recreate(temp, settings);
// }
} }
} }
else else
{ {
cert = request.CreateSelfSigned(notBefore, notAfter); cert = request.CreateSelfSigned(notBefore, notAfter);
// using (X509Certificate2 temp = request.CreateSelfSigned(notBefore, notAfter))
// {
// // Generated instance of the cert can't be added to cert-store (private key is missing) if requested by the caller.
// // To avoid this recreate the cert
// cert = Recreate(temp, settings);
// }
} }
} }
@@ -170,7 +158,7 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
{ {
try try
{ {
AsymmetricAlgorithm pk = issuerCertificate.PrivateKey; AsymmetricAlgorithm? pk = issuerCertificate.PrivateKey;
throw new CertGenException("Unsupported type of private-key: '{0}'", pk?.GetType().FullName ?? "<null>"); throw new CertGenException("Unsupported type of private-key: '{0}'", pk?.GetType().FullName ?? "<null>");
} }
catch (CertGenException) catch (CertGenException)
@@ -201,22 +189,13 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
return serial; return serial;
} }
private X509Certificate2 Recreate(X509Certificate2 source, CertificateSettings settings)
{
byte[] data = source.Export(X509ContentType.Pfx, string.Empty);
X509KeyStorageFlags flags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet;
if (!settings.ExportableKeys)
{
flags &= ~X509KeyStorageFlags.Exportable;
}
X509Certificate2 target = X509CertificateLoader.LoadPkcs12(data, string.Empty);
target.FriendlyName = settings.FriendlyName;
return target;
}
private string CreateCommonName(string? subjectName) private string CreateCommonName(string? subjectName)
{ {
if (subjectName == null)
{
throw new CertGenException("Cannot create common-name for certificate as subject-name is null");
}
string cn; string cn;
if (subjectName.StartsWith("CN=", StringComparison.OrdinalIgnoreCase)) if (subjectName.StartsWith("CN=", StringComparison.OrdinalIgnoreCase))

View File

@@ -2,7 +2,7 @@
namespace CertMgr.CertGen.Utils; namespace CertMgr.CertGen.Utils;
public sealed class CollectionEquivalencyComparer<T> : IEqualityComparer<IEnumerable<T>> public sealed class CollectionEquivalencyComparer<T> : IEqualityComparer<IEnumerable<T>> where T : notnull
{ {
public bool Equals(IEnumerable<T>? x, IEnumerable<T>? y) public bool Equals(IEnumerable<T>? x, IEnumerable<T>? y)
{ {

View File

@@ -8,6 +8,7 @@ public sealed class SettingAttribute : Attribute
{ {
Name = name; Name = name;
IsMandatory = false; IsMandatory = false;
AlternateNames = Array.Empty<string>();
} }
public string Name { [DebuggerStepThrough] get; } public string Name { [DebuggerStepThrough] get; }

View File

@@ -6,7 +6,7 @@ public sealed class CliParser
{ {
public Task<RawArguments> ParseAsync(string[] args, CancellationToken cancellationToken) public Task<RawArguments> ParseAsync(string[] args, CancellationToken cancellationToken)
{ {
CLog.Info("parsing arguments..."); CLog.Info("Parsing arguments...");
RawArguments rawArgs = new RawArguments(args.Length); RawArguments rawArgs = new RawArguments(args.Length);
@@ -54,7 +54,7 @@ public sealed class CliParser
} }
} }
CLog.Info("parsing arguments... done (found {0} arguments)", rawArgs.Count); CLog.Info("Parsing arguments... done (found {0} arguments)", rawArgs.Count);
return Task.FromResult(rawArgs); return Task.FromResult(rawArgs);
} }

View File

@@ -4,7 +4,7 @@ public sealed class StringConverter : ValueConverter<string>
{ {
protected override Task<string?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) protected override Task<string?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{ {
return Task.FromResult(rawValue); return Task.FromResult((string?)rawValue);
} }
} }

View File

@@ -15,7 +15,11 @@ public sealed class TimeSpanConverter : ValueConverter<TimeSpan?>
TimeSpan? result; TimeSpan? result;
if (int.TryParse(valueSpan, out int value)) if (TimeSpan.TryParse(valueSpan, out TimeSpan tmp))
{
result = tmp;
}
else if (int.TryParse(valueSpan, out int value))
{ {
switch (unit) switch (unit)
{ {

View File

@@ -1,4 +1,5 @@
using System.Reflection; using System.Diagnostics;
using System.Reflection;
using System.Text; using System.Text;
using CertMgr.Core.Cli; using CertMgr.Core.Cli;
@@ -24,10 +25,32 @@ internal sealed class JobExecutor
if (jobs.TryGet(rawArg.Values.First(), out JobDescriptor? descriptor)) if (jobs.TryGet(rawArg.Values.First(), out JobDescriptor? descriptor))
{ {
IJob job = await CreateJobAsync(descriptor, rawArgs, cancellationToken).ConfigureAwait(false); IJob job;
try
{
job = await CreateJobAsync(descriptor, rawArgs, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
CLog.Error(e, "Failed to instantiate job '{0}'", rawArg.Values.First());
throw new JobException(e, "Failed to instantiate job '{0}'", rawArg.Values.First());
}
Stopwatch sw = Stopwatch.StartNew();
try
{
CLog.Info("Executing job '{0}'...", job.Name);
JobResult result = await job.ExecuteAsync(cancellationToken).ConfigureAwait(false); JobResult result = await job.ExecuteAsync(cancellationToken).ConfigureAwait(false);
errorLevel = result.ErrorLevel; errorLevel = result.ErrorLevel;
CLog.Info("Executing job '{0}'... done (finished with error-level {1}, took {2})", job.Name, errorLevel, sw.Elapsed);
}
catch (Exception e)
{
CLog.Info("Executing job '{0}'... done (threw exception after {1})", job.Name, sw.Elapsed);
throw new JobException(e, "Failed to execute job '{0}'", job.Name);
}
} }
else else
{ {

View File

@@ -48,9 +48,16 @@ internal sealed class JobRegistry
else else
{ {
if (jobName == null && settingsType != null) if (jobName == null && settingsType != null)
{
if (errorMessage != null)
{ {
CLog.Error(errorMessage); CLog.Error(errorMessage);
} }
else
{
CLog.Error("Failed to get name of job from type '{0}'", type.ToString(false));
}
}
if (jobName != null && settingsType == null) if (jobName != null && settingsType == null)
{ {
CLog.Error("Job '{0}' has job-name '{1}' but the class doesn't inherit from '{2}'", type.ToString(false), jobName, typeof(Job<>).ToString(false)); CLog.Error("Job '{0}' has job-name '{1}' but the class doesn't inherit from '{2}'", type.ToString(false), jobName, typeof(Job<>).ToString(false));
@@ -59,7 +66,7 @@ internal sealed class JobRegistry
} }
} }
CLog.Info("Loading job registry... done (registered {0} jobs)", _items.Count); CLog.Info("Loading job registry... done (found {0} jobs)", _items.Count);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -9,6 +9,7 @@ using CertMgr.Core.Cli;
using CertMgr.Core.Converters; using CertMgr.Core.Converters;
using CertMgr.Core.Jobs; using CertMgr.Core.Jobs;
using CertMgr.Core.Log; using CertMgr.Core.Log;
using CertMgr.Core.Utils;
using CertMgr.Core.Validation; using CertMgr.Core.Validation;
namespace CertMgr.Core; namespace CertMgr.Core;
@@ -34,7 +35,9 @@ internal sealed class SettingsBuilder
{ {
(bool isCollection, Type elementType) = GetValueType(propertyInfo); (bool isCollection, Type elementType) = GetValueType(propertyInfo);
(bool converted, object? convertedValue) = await ConvertRawValueAsync(settingAttribute, rawArg, isCollection, elementType, cancellationToken).ConfigureAwait(false); try
{
(bool converted, object? convertedValue) = await ConvertRawValueAsync(settingAttribute, rawArg, isCollection, propertyInfo.PropertyType, elementType, cancellationToken).ConfigureAwait(false);
if (converted) if (converted)
{ {
propertyInfo.SetValue(settings, convertedValue); propertyInfo.SetValue(settings, convertedValue);
@@ -50,6 +53,12 @@ internal sealed class SettingsBuilder
} }
} }
} }
catch (Exception e)
{
CLog.Error(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
throw new CertMgrException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
}
}
else if (settingAttribute.IsMandatory) else if (settingAttribute.IsMandatory)
{ {
ValidationResult valres = new ValidationResult(settingAttribute.Name, false, "Mandatory argument is missing"); ValidationResult valres = new ValidationResult(settingAttribute.Name, false, "Mandatory argument is missing");
@@ -65,7 +74,7 @@ internal sealed class SettingsBuilder
} }
else else
{ {
CLog.Error("Default value for argument '{0}' is specified, but its type is '{1}' instead of expected '{2}'", settingAttribute.Name, settingAttribute.Default?.GetType().Name ?? "<null>", elementType); CLog.Error("Default value for argument '{0}' is specified, but its type is '{1}' instead of expected '{2}'", settingAttribute.Name, settingAttribute.Default?.GetType().ToString(false) ?? "<null>", elementType.ToString(false));
} }
} }
} }
@@ -141,7 +150,7 @@ internal sealed class SettingsBuilder
return settings; return settings;
} }
private async Task<(bool success, object? convertedValue)> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type targetType, CancellationToken cancellationToken) private async Task<(bool success, object? convertedValue)> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type collectionType, Type elementType, CancellationToken cancellationToken)
{ {
bool success = false; bool success = false;
object? convertedValue = null; object? convertedValue = null;
@@ -150,10 +159,12 @@ internal sealed class SettingsBuilder
{ {
if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter)) if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter))
{ {
List<object?> values = new List<object?>(); Type listType = typeof(List<>).MakeGenericType(elementType);
IList values = (IList)Activator.CreateInstance(listType)!;
foreach (string rawValue in rawArg.Values) foreach (string rawValue in rawArg.Values)
{ {
convertedValue = await customConverter.ConvertAsync(rawValue, targetType, cancellationToken).ConfigureAwait(false); convertedValue = await customConverter.ConvertAsync(rawValue, elementType, cancellationToken).ConfigureAwait(false);
values.Add(convertedValue); values.Add(convertedValue);
} }
convertedValue = values; convertedValue = values;
@@ -161,15 +172,17 @@ internal sealed class SettingsBuilder
} }
else else
{ {
List<object?> values = new List<object?>(); Type listType = typeof(List<>).MakeGenericType(elementType);
IList values = (IList)Activator.CreateInstance(listType)!;
foreach (string rawValue in rawArg.Values) foreach (string rawValue in rawArg.Values)
{ {
if (TryConvertValue(rawValue, targetType, out convertedValue)) if (TryConvertValue(rawValue, elementType, out convertedValue))
{ {
values.Add(convertedValue); values.Add(convertedValue);
} }
} }
convertedValue = values; convertedValue = values; // BuildCollectionValue(collectionType, elementType, values);
success = true; success = true;
} }
} }
@@ -177,10 +190,10 @@ internal sealed class SettingsBuilder
{ {
if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter)) if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter))
{ {
convertedValue = await customConverter.ConvertAsync(rawArg.Values.First(), targetType, cancellationToken).ConfigureAwait(false); convertedValue = await customConverter.ConvertAsync(rawArg.Values.First(), elementType, cancellationToken).ConfigureAwait(false);
success = true; success = true;
} }
else if (TryConvertValue(rawArg.Values.First(), targetType, out convertedValue)) else if (TryConvertValue(rawArg.Values.First(), elementType, out convertedValue))
{ {
success = true; success = true;
} }
@@ -192,6 +205,58 @@ internal sealed class SettingsBuilder
return (success, convertedValue); return (success, convertedValue);
} }
private static object BuildCollectionValue(Type collectionType, Type elementType, IReadOnlyList<object?> items)
{
// convert source collection with 'items' of type 'object?' to collection with items of requested type:
Type listType = typeof(List<>).MakeGenericType(elementType);
IList typedList = (IList)Activator.CreateInstance(listType)!;
foreach (object? item in items)
{
typedList.Add(item);
}
if (collectionType.IsArray)
{
Array array = Array.CreateInstance(elementType, typedList.Count);
typedList.CopyTo(array, 0);
return array;
}
// it is either IEnumerable<T> or ICollection<T> or IList<T> or List<T>
if (collectionType.IsAssignableFrom(listType))
{
return typedList;
}
// we do not know the type, but try to instantiate 'collectionType' and if it implements ICollection<>, then fill it
object? instance = Activator.CreateInstance(collectionType);
if (instance != null)
{
Type? iCollectionT = collectionType
.GetInterfaces()
.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollection<>));
if (iCollectionT != null)
{
MethodInfo? add = iCollectionT.GetMethod("Add");
if (add != null)
{
foreach (object? item in typedList)
{
add.Invoke(instance, new object?[] { item });
}
return instance;
}
}
}
// try it as array, we fail anyway
Array fallback = Array.CreateInstance(elementType, typedList.Count);
typedList.CopyTo(fallback, 0);
return fallback;
}
private bool TryGetCustomConverter(SettingAttribute settingAttribute, [NotNullWhen(true)] out IValueConverter? customConverter) private bool TryGetCustomConverter(SettingAttribute settingAttribute, [NotNullWhen(true)] out IValueConverter? customConverter)
{ {
customConverter = null; customConverter = null;
@@ -266,67 +331,14 @@ internal sealed class SettingsBuilder
return (true, type.GetGenericArguments()[0]); return (true, type.GetGenericArguments()[0]);
} }
if (type.GetInterfaces().Any(i => foreach (Type i in type.GetInterfaces())
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{ {
return (true, type.GetGenericArguments()[0]); if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return (true, i.GetGenericArguments()[0]);
}
} }
// if (type.IsGenericType && typeof(IEnumerable<>).IsAssignableFrom(type.GetGenericTypeDefinition()))
// {
// return (true, type.GetGenericArguments()[0]);
// }
// if (t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>)))
// {
// return (true, t.GetInterfaces().First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>)).GetGenericArguments()[0]);
// }
// if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(List<>)))
// {
// return (true, t.GetGenericArguments()[0]);
// }
return (false, null); return (false, null);
} }
private static bool IsEnumerableType(Type type)
{
if (type == null)
{
return false;
}
if (type == typeof(string))
{
return false;
}
if (type.IsArray)
{
return true;
}
if (typeof(IEnumerable).IsAssignableFrom(type))
{
return true;
}
if (type.GetInterfaces().Any(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
return true;
}
return false;
}
private static Type UnwrapNullable(Type type)
{
Type? underlying = Nullable.GetUnderlyingType(type);
if (underlying != null)
{
return underlying;
}
return type;
}
} }

View File

@@ -126,7 +126,7 @@ internal static class Extenders
} }
} }
public static bool Equivalent<T>(this IEnumerable<T?> left, IEnumerable<T?> right, IEqualityComparer<T?>? comparer = null) public static bool Equivalent<T>(this IEnumerable<T?> left, IEnumerable<T?> right, IEqualityComparer<T?>? comparer = null) where T : notnull
{ {
if (ReferenceEquals(left, right)) if (ReferenceEquals(left, right))
{ {

View File

@@ -99,7 +99,6 @@ internal static class StringBuilderCache
if (Interlocked.Exchange(ref _disposed, 1) == 0) if (Interlocked.Exchange(ref _disposed, 1) == 0)
{ {
StringBuilder? sb = _sb; StringBuilder? sb = _sb;
_sb = null;
Release(sb); Release(sb);
} }
} }
@@ -121,7 +120,7 @@ internal static class StringBuilderCache
return s; return s;
} }
public StringBuilder Builder => _sb; public StringBuilder Builder => _disposed == 0 ? _sb : throw new ObjectDisposedException(nameof(ScopedBuilder));
public static implicit operator StringBuilder(ScopedBuilder b) public static implicit operator StringBuilder(ScopedBuilder b)
{ {

View File

@@ -12,6 +12,8 @@ internal static class StringFormatter
} }
catch (Exception e) catch (Exception e)
{ {
e.GetType();
using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(); using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped();
StringBuilder sb = lease.Builder; StringBuilder sb = lease.Builder;

View File

@@ -40,7 +40,7 @@ public sealed class CertificateSettings : JobSettings
[Setting("is-certificate-authority", Default = false, AlternateNames = ["isca"])] [Setting("is-certificate-authority", Default = false, AlternateNames = ["isca"])]
public bool IsCertificateAuthority { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } public bool IsCertificateAuthority { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("issuer", Converter = typeof(StorageConverter))] [Setting("issuer-certificate", Converter = typeof(StorageConverter))]
public IStorage? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } public IStorage? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("issuer-password")] [Setting("issuer-password")]

View File

@@ -6,8 +6,17 @@ internal static class Program
{ {
private static async Task<int> Main(string[] args) private static async Task<int> Main(string[] args)
{ {
args = ["--job=create-certificate", "--issuer=file|o|c:\\friend2.pfx", "--issuer-password=aaa", "--subject=hello", "--san=world", "--algorithm=ecdsa", "--ecdsa-curve=p384", "--storage=file|w|c:\\mycert.pfx", "--validity-period=2d"]; args = [
// args = ["--job=create-certificate", "--subject=hello", "--algorithm=ecdsa", "--curve=p384"]; "--job=create-certificate",
"--issuer-certificate=file|o|c:\\friend2.pfx",
"--issuer-password=aaa",
"--subject=hello",
"--san=world",
"--algorithm=ecdsa",
"--ecdsa-curve=p384",
"--storage=file|w|c:\\mycert.pfx",
"--validity-period=2d" ];
using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
JobExecutor executor = new JobExecutor(); JobExecutor executor = new JobExecutor();