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))
{
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
{
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
{
AsymmetricAlgorithm pk = issuerCertificate.PrivateKey;
AsymmetricAlgorithm? pk = issuerCertificate.PrivateKey;
throw new CertGenException("Unsupported type of private-key: '{0}'", pk?.GetType().FullName ?? "<null>");
}
catch (CertGenException)
@@ -201,22 +189,13 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
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)
{
if (subjectName == null)
{
throw new CertGenException("Cannot create common-name for certificate as subject-name is null");
}
string cn;
if (subjectName.StartsWith("CN=", StringComparison.OrdinalIgnoreCase))

View File

@@ -2,7 +2,7 @@
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)
{

View File

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

View File

@@ -6,7 +6,7 @@ public sealed class CliParser
{
public Task<RawArguments> ParseAsync(string[] args, CancellationToken cancellationToken)
{
CLog.Info("parsing arguments...");
CLog.Info("Parsing arguments...");
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);
}

View File

@@ -4,7 +4,7 @@ public sealed class StringConverter : ValueConverter<string>
{
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;
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)
{

View File

@@ -1,4 +1,5 @@
using System.Reflection;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using CertMgr.Core.Cli;
@@ -24,10 +25,32 @@ internal sealed class JobExecutor
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);
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
{

View File

@@ -48,9 +48,16 @@ internal sealed class JobRegistry
else
{
if (jobName == null && settingsType != null)
{
if (errorMessage != null)
{
CLog.Error(errorMessage);
}
else
{
CLog.Error("Failed to get name of job from type '{0}'", type.ToString(false));
}
}
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));
@@ -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;
}

View File

@@ -9,6 +9,7 @@ using CertMgr.Core.Cli;
using CertMgr.Core.Converters;
using CertMgr.Core.Jobs;
using CertMgr.Core.Log;
using CertMgr.Core.Utils;
using CertMgr.Core.Validation;
namespace CertMgr.Core;
@@ -34,7 +35,9 @@ internal sealed class SettingsBuilder
{
(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)
{
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)
{
ValidationResult valres = new ValidationResult(settingAttribute.Name, false, "Mandatory argument is missing");
@@ -65,7 +74,7 @@ internal sealed class SettingsBuilder
}
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;
}
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;
object? convertedValue = null;
@@ -150,10 +159,12 @@ internal sealed class SettingsBuilder
{
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)
{
convertedValue = await customConverter.ConvertAsync(rawValue, targetType, cancellationToken).ConfigureAwait(false);
convertedValue = await customConverter.ConvertAsync(rawValue, elementType, cancellationToken).ConfigureAwait(false);
values.Add(convertedValue);
}
convertedValue = values;
@@ -161,15 +172,17 @@ internal sealed class SettingsBuilder
}
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)
{
if (TryConvertValue(rawValue, targetType, out convertedValue))
if (TryConvertValue(rawValue, elementType, out convertedValue))
{
values.Add(convertedValue);
}
}
convertedValue = values;
convertedValue = values; // BuildCollectionValue(collectionType, elementType, values);
success = true;
}
}
@@ -177,10 +190,10 @@ internal sealed class SettingsBuilder
{
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;
}
else if (TryConvertValue(rawArg.Values.First(), targetType, out convertedValue))
else if (TryConvertValue(rawArg.Values.First(), elementType, out convertedValue))
{
success = true;
}
@@ -192,6 +205,58 @@ internal sealed class SettingsBuilder
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)
{
customConverter = null;
@@ -266,67 +331,14 @@ internal sealed class SettingsBuilder
return (true, type.GetGenericArguments()[0]);
}
if (type.GetInterfaces().Any(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
foreach (Type i in type.GetInterfaces())
{
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);
}
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))
{

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ public sealed class CertificateSettings : JobSettings
[Setting("is-certificate-authority", Default = false, AlternateNames = ["isca"])]
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; }
[Setting("issuer-password")]

View File

@@ -6,8 +6,17 @@ internal static class Program
{
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 = ["--job=create-certificate", "--subject=hello", "--algorithm=ecdsa", "--curve=p384"];
args = [
"--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));
JobExecutor executor = new JobExecutor();