diff --git a/BuildOutput/bin/Debug/net9.0/certmgr.dll b/BuildOutput/bin/Debug/net9.0/certmgr.dll index e305eb8..8fdfb44 100644 Binary files a/BuildOutput/bin/Debug/net9.0/certmgr.dll and b/BuildOutput/bin/Debug/net9.0/certmgr.dll differ diff --git a/BuildOutput/bin/Debug/net9.0/certmgr.exe b/BuildOutput/bin/Debug/net9.0/certmgr.exe index 91561b7..f588920 100644 Binary files a/BuildOutput/bin/Debug/net9.0/certmgr.exe and b/BuildOutput/bin/Debug/net9.0/certmgr.exe differ diff --git a/BuildOutput/bin/Debug/net9.0/certmgr.pdb b/BuildOutput/bin/Debug/net9.0/certmgr.pdb index 7e6392b..cd7dc15 100644 Binary files a/BuildOutput/bin/Debug/net9.0/certmgr.pdb and b/BuildOutput/bin/Debug/net9.0/certmgr.pdb differ diff --git a/certmgr/CertGen/CertificateGeneratorBase.cs b/certmgr/CertGen/CertificateGeneratorBase.cs index d89ad83..6a5d68d 100644 --- a/certmgr/CertGen/CertificateGeneratorBase.cs +++ b/certmgr/CertGen/CertificateGeneratorBase.cs @@ -117,23 +117,11 @@ internal abstract class CertificateGeneratorBase : 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 : ICerti { try { - AsymmetricAlgorithm pk = issuerCertificate.PrivateKey; + AsymmetricAlgorithm? pk = issuerCertificate.PrivateKey; throw new CertGenException("Unsupported type of private-key: '{0}'", pk?.GetType().FullName ?? ""); } catch (CertGenException) @@ -201,22 +189,13 @@ internal abstract class CertificateGeneratorBase : 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)) diff --git a/certmgr/CertGen/Utils/CollectionEquivalencyComparer.cs b/certmgr/CertGen/Utils/CollectionEquivalencyComparer.cs index 1708e80..1dee062 100644 --- a/certmgr/CertGen/Utils/CollectionEquivalencyComparer.cs +++ b/certmgr/CertGen/Utils/CollectionEquivalencyComparer.cs @@ -2,7 +2,7 @@ namespace CertMgr.CertGen.Utils; -public sealed class CollectionEquivalencyComparer : IEqualityComparer> +public sealed class CollectionEquivalencyComparer : IEqualityComparer> where T : notnull { public bool Equals(IEnumerable? x, IEnumerable? y) { diff --git a/certmgr/Core/Attributes/SettingAttribute.cs b/certmgr/Core/Attributes/SettingAttribute.cs index 5736a00..96b489d 100644 --- a/certmgr/Core/Attributes/SettingAttribute.cs +++ b/certmgr/Core/Attributes/SettingAttribute.cs @@ -8,6 +8,7 @@ public sealed class SettingAttribute : Attribute { Name = name; IsMandatory = false; + AlternateNames = Array.Empty(); } public string Name { [DebuggerStepThrough] get; } diff --git a/certmgr/Core/Cli/CliParser.cs b/certmgr/Core/Cli/CliParser.cs index b59edbb..1ce208c 100644 --- a/certmgr/Core/Cli/CliParser.cs +++ b/certmgr/Core/Cli/CliParser.cs @@ -6,7 +6,7 @@ public sealed class CliParser { public Task 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); } diff --git a/certmgr/Core/Converters/Impl/StringConverter.cs b/certmgr/Core/Converters/Impl/StringConverter.cs index 4f66aa9..bbccfdc 100644 --- a/certmgr/Core/Converters/Impl/StringConverter.cs +++ b/certmgr/Core/Converters/Impl/StringConverter.cs @@ -4,7 +4,7 @@ public sealed class StringConverter : ValueConverter { protected override Task DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) { - return Task.FromResult(rawValue); + return Task.FromResult((string?)rawValue); } } diff --git a/certmgr/Core/Converters/Impl/TimeSpanConverter.cs b/certmgr/Core/Converters/Impl/TimeSpanConverter.cs index 9b54015..d814d86 100644 --- a/certmgr/Core/Converters/Impl/TimeSpanConverter.cs +++ b/certmgr/Core/Converters/Impl/TimeSpanConverter.cs @@ -15,7 +15,11 @@ public sealed class TimeSpanConverter : ValueConverter 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) { diff --git a/certmgr/Core/JobExecutor.cs b/certmgr/Core/JobExecutor.cs index 479f1c7..293e0f5 100644 --- a/certmgr/Core/JobExecutor.cs +++ b/certmgr/Core/JobExecutor.cs @@ -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()); + } - JobResult result = await job.ExecuteAsync(cancellationToken).ConfigureAwait(false); - errorLevel = result.ErrorLevel; + 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 { diff --git a/certmgr/Core/JobRegistry.cs b/certmgr/Core/JobRegistry.cs index 29ae281..e654cd7 100644 --- a/certmgr/Core/JobRegistry.cs +++ b/certmgr/Core/JobRegistry.cs @@ -49,7 +49,14 @@ internal sealed class JobRegistry { if (jobName == null && settingsType != null) { - CLog.Error(errorMessage); + 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) { @@ -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; } diff --git a/certmgr/Core/SettingsBuilder.cs b/certmgr/Core/SettingsBuilder.cs index 75ad264..dfe1b61 100644 --- a/certmgr/Core/SettingsBuilder.cs +++ b/certmgr/Core/SettingsBuilder.cs @@ -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,21 +35,29 @@ internal sealed class SettingsBuilder { (bool isCollection, Type elementType) = GetValueType(propertyInfo); - (bool converted, object? convertedValue) = await ConvertRawValueAsync(settingAttribute, rawArg, isCollection, elementType, cancellationToken).ConfigureAwait(false); - if (converted) + try { - propertyInfo.SetValue(settings, convertedValue); - - if (settingAttribute.Validator != null) + (bool converted, object? convertedValue) = await ConvertRawValueAsync(settingAttribute, rawArg, isCollection, propertyInfo.PropertyType, elementType, cancellationToken).ConfigureAwait(false); + if (converted) { - ISettingValidator? validator = (ISettingValidator?)Activator.CreateInstance(settingAttribute.Validator, [settingAttribute.Name]); - if (validator != null) + propertyInfo.SetValue(settings, convertedValue); + + if (settingAttribute.Validator != null) { - ValidationResult valres = await validator.ValidateAsync(convertedValue, cancellationToken).ConfigureAwait(false); - settings.ValidationResults.Add(valres); + ISettingValidator? validator = (ISettingValidator?)Activator.CreateInstance(settingAttribute.Validator, [settingAttribute.Name]); + if (validator != null) + { + ValidationResult valres = await validator.ValidateAsync(convertedValue, cancellationToken).ConfigureAwait(false); + settings.ValidationResults.Add(valres); + } } } } + 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) { @@ -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 ?? "", 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) ?? "", 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 values = new List(); + 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 values = new List(); + 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 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 or ICollection or IList or List + 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; - } } diff --git a/certmgr/Core/Utils/Extenders.cs b/certmgr/Core/Utils/Extenders.cs index 194e06e..ffac83b 100644 --- a/certmgr/Core/Utils/Extenders.cs +++ b/certmgr/Core/Utils/Extenders.cs @@ -126,7 +126,7 @@ internal static class Extenders } } - public static bool Equivalent(this IEnumerable left, IEnumerable right, IEqualityComparer? comparer = null) + public static bool Equivalent(this IEnumerable left, IEnumerable right, IEqualityComparer? comparer = null) where T : notnull { if (ReferenceEquals(left, right)) { diff --git a/certmgr/Core/Utils/StringBuilderCache.cs b/certmgr/Core/Utils/StringBuilderCache.cs index 447975f..e4d2186 100644 --- a/certmgr/Core/Utils/StringBuilderCache.cs +++ b/certmgr/Core/Utils/StringBuilderCache.cs @@ -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) { diff --git a/certmgr/Core/Utils/StringFormatter.cs b/certmgr/Core/Utils/StringFormatter.cs index fbc0071..63451c6 100644 --- a/certmgr/Core/Utils/StringFormatter.cs +++ b/certmgr/Core/Utils/StringFormatter.cs @@ -12,6 +12,8 @@ internal static class StringFormatter } catch (Exception e) { + e.GetType(); + using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(); StringBuilder sb = lease.Builder; diff --git a/certmgr/Jobs/CertificateSettings.cs b/certmgr/Jobs/CertificateSettings.cs index b7148bb..0b7378d 100644 --- a/certmgr/Jobs/CertificateSettings.cs +++ b/certmgr/Jobs/CertificateSettings.cs @@ -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")] diff --git a/certmgr/Program.cs b/certmgr/Program.cs index 68e563f..83a9dc1 100644 --- a/certmgr/Program.cs +++ b/certmgr/Program.cs @@ -6,8 +6,17 @@ internal static class Program { private static async Task 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();