diff --git a/BuildOutput/bin/Debug/net9.0/certmgr.dll b/BuildOutput/bin/Debug/net9.0/certmgr.dll index 0503a3e..a5fcb21 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 327056e..9a5164c 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 18bafc1..fdd65cb 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/Core/Converters/Impl/CertStoreStorageContextConverter.cs b/certmgr/Core/Converters/Impl/CertStoreStorageContextConverter.cs new file mode 100644 index 0000000..ab75750 --- /dev/null +++ b/certmgr/Core/Converters/Impl/CertStoreStorageContextConverter.cs @@ -0,0 +1,125 @@ +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography.X509Certificates; + +using CertMgr.Core.Log; +using CertMgr.Core.Storage; +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Converters.Impl; + +internal sealed class CertStoreStorageContextConverter : StorageContextConverter +{ + protected override Task DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) + { + ReadOnlySpan storageDefinition = rawValue.AsSpan(); + + TryGetCertStore(storageDefinition, out CertStoreStorageContext? context); + + return Task.FromResult(context); + } + + private bool TryGetCertStore(ReadOnlySpan storageDefinition, [NotNullWhen(true)] out CertStoreStorageContext? context) + { + // expecting 'storageDefinition' is something like: + // || + // where can be: + // - 'user' + // - 'machine' + // where ' can be: + // - 'my' + // - 'root' + // where can be: + // - 'default' + // - 'exportable' + + int firstSplitIndex = storageDefinition.IndexOf(Separator); + if (firstSplitIndex != -1) + { + // there is a splitter. On the left side of it there must be . On the right there is a + + ReadOnlySpan locationSpan = storageDefinition.Slice(0, firstSplitIndex); + int secondSplitIndex = storageDefinition.Slice(firstSplitIndex + 1).IndexOf(Separator) + locationSpan.Length + 1; + ReadOnlySpan nameSpan = storageDefinition.Slice(firstSplitIndex + 1, secondSplitIndex - firstSplitIndex - 1); + ReadOnlySpan flagsSpan = storageDefinition.Slice(secondSplitIndex + 1); + + CertStoreLocation? storeLocation = null; + CertStoreName? storeName = null; + X509KeyStorageFlags? flags = null; + if (locationSpan.Equals("user", StringComparison.OrdinalIgnoreCase)) + { + storeLocation = CertStoreLocation.User; + } + else if (locationSpan.Equals("machine", StringComparison.OrdinalIgnoreCase)) + { + storeLocation = CertStoreLocation.Machine; + } + else + { + CLog.Error("{0}: Unsupported store-location '{1}' for storage '{2}'", GetType().ToString(false), locationSpan.ToString(), typeof(CertStoreStorage).ToString(false)); + } + + if (nameSpan.Equals("my", StringComparison.OrdinalIgnoreCase)) + { + storeName = CertStoreName.My; + } + else if (nameSpan.Equals("root", StringComparison.OrdinalIgnoreCase)) + { + storeName = CertStoreName.Root; + } + else + { + CLog.Error("{0}: Unsupported store-name '{1}' for storage '{2}'", GetType().ToString(false), nameSpan.ToString(), typeof(CertStoreStorage).ToString(false)); + } + + if (flagsSpan.Length > 0) + { + MemoryExtensions.SpanSplitEnumerator enu = flagsSpan.Split(','); + while (enu.MoveNext()) + { + ReadOnlySpan currentExt = flagsSpan[enu.Current].Trim(); + if (currentExt.Equals("default", StringComparison.OrdinalIgnoreCase)) + { + if (flags.HasValue) + { + flags |= X509KeyStorageFlags.DefaultKeySet; + } + else + { + flags = X509KeyStorageFlags.DefaultKeySet; + } + } + else if (currentExt.Equals("exportable", StringComparison.OrdinalIgnoreCase)) + { + if (flags.HasValue) + { + flags |= X509KeyStorageFlags.Exportable; + } + else + { + flags = X509KeyStorageFlags.Exportable; + } + } + } + } + else + { + CLog.Error("{0}: cert storage-flags not defined", GetType().ToString(false)); + } + + if (storeLocation.HasValue && storeName.HasValue) + { + context = new CertStoreStorageContext(storeLocation.Value, storeName.Value, flags.Value); + } + else + { + context = null; + } + } + else + { + context = null; + } + + return context != null; + } +} diff --git a/certmgr/Core/Converters/Impl/FileStorageContextConverter.cs b/certmgr/Core/Converters/Impl/FileStorageContextConverter.cs new file mode 100644 index 0000000..e3ec12f --- /dev/null +++ b/certmgr/Core/Converters/Impl/FileStorageContextConverter.cs @@ -0,0 +1,99 @@ +using System.Diagnostics.CodeAnalysis; + +using CertMgr.Core.Log; +using CertMgr.Core.Storage; +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Converters.Impl; + +internal class FileStorageContextConverter : StorageContextConverter +{ + protected override Task DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) + { + ReadOnlySpan storageDefinition = rawValue.AsSpan(); + + TryGetFileStore(storageDefinition, out FileStorageContext? context); + + return Task.FromResult(context); + } + + private bool TryGetFileStore(ReadOnlySpan storageDefinition, [NotNullWhen(true)] out FileStorageContext? context) + { + // expecting that 'storageDefinition' is something like: + // | + // or + // | + // or + // + // + // where can be: + // 'o' or 'overwrite' or 'overwriteornew' + // or + // 'a' or 'append' or 'appendornew' + // or + // 'c' or 'create' or 'createnew' + // + // where can be: + // 'c:\path\myfile.txt' + // or + // '/path/myfile.txt' + // or + // './path/myfile.txt' + // in addition - can be either quoted or double-quoted + + context = null; + + FileStoreMode defaultStoreMode = FileStoreMode.OverwriteOrNew; + FileStoreMode storeMode; + ReadOnlySpan filename; + + int firstSplitIndex = storageDefinition.IndexOf(Separator); + if (firstSplitIndex != -1) + { + // there is a splitter. On the left side of it there must be . On the right there is a + + ReadOnlySpan firstPart = storageDefinition.Slice(0, firstSplitIndex); + filename = storageDefinition.Slice(firstSplitIndex + 1); + + if (firstPart.Length == 0) + { + storeMode = defaultStoreMode; + } + else if (Enum.TryParse(firstPart, true, out FileStoreMode tmpMode)) + { + storeMode = tmpMode; + } + else if (firstPart.Equals("w", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("overwrite", StringComparison.OrdinalIgnoreCase)) + { + storeMode = FileStoreMode.OverwriteOrNew; + } + else if (firstPart.Equals("c", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("createnew", StringComparison.OrdinalIgnoreCase)) + { + storeMode = FileStoreMode.Create; + } + else if (firstPart.Equals("a", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("append", StringComparison.OrdinalIgnoreCase)) + { + storeMode = FileStoreMode.AppendOrNew; + } + else if (firstPart.Equals("o", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("open", StringComparison.OrdinalIgnoreCase)) + { + storeMode = FileStoreMode.Open; + } + else + { + // it is not store-mode or there is a typo or unsupported value + CLog.Error(string.Format("{0}: Unsupported store-mode '{1}' for storage '{2}'", GetType().ToString(false), firstPart.ToString()), typeof(FileStorage).ToString(false)); + storeMode = defaultStoreMode; + } + } + else + { + // no split-char => just filename + storeMode = defaultStoreMode; + filename = storageDefinition; + } + + context = new FileStorageContext(filename.ToString(), storeMode); + return true; + } +} diff --git a/certmgr/Core/Converters/Impl/StorageContextConverter.cs b/certmgr/Core/Converters/Impl/StorageContextConverter.cs new file mode 100644 index 0000000..2e338f2 --- /dev/null +++ b/certmgr/Core/Converters/Impl/StorageContextConverter.cs @@ -0,0 +1,8 @@ +using CertMgr.Core.Storage; + +namespace CertMgr.Core.Converters.Impl; + +public abstract class StorageContextConverter : ValueConverter where T : StorageContext +{ + protected const char Separator = '|'; +} diff --git a/certmgr/Core/Converters/Impl/StorageConverter.cs b/certmgr/Core/Converters/Impl/StorageConverter.cs index bc334d8..9fd2d58 100644 --- a/certmgr/Core/Converters/Impl/StorageConverter.cs +++ b/certmgr/Core/Converters/Impl/StorageConverter.cs @@ -1,7 +1,9 @@ -using System.Diagnostics.CodeAnalysis; +/*using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography.X509Certificates; using CertMgr.Core.Log; using CertMgr.Core.Storage; +using CertMgr.Core.Utils; namespace CertMgr.Core.Converters.Impl; @@ -31,6 +33,13 @@ internal sealed class StorageConverter : ValueConverter storage = EmptyStorage.Empty; } break; + case "certstore": + case "cert-store": + if (!TryGetCertStore(storageDefinition, out storage)) + { + storage = EmptyStorage.Empty; + } + break; default: storage = EmptyStorage.Empty; break; @@ -104,7 +113,7 @@ internal sealed class StorageConverter : ValueConverter else { // it is not store-mode or there is a typo or unsupported value - CLog.Error(string.Concat("Unsupported store-mode '", firstPart, "'")); + CLog.Error(string.Format("{0}: Unsupported store-mode '{1}' for storage '{2}'", GetType().ToString(false), firstPart.ToString()), typeof(FileStorage).ToString(false)); storeMode = defaultStoreMode; } } @@ -118,4 +127,69 @@ internal sealed class StorageConverter : ValueConverter storage = new FileStorage(filename.ToString(), storeMode); return true; } + + private bool TryGetCertStore(ReadOnlySpan storageDefinition, [NotNullWhen(true)] out IStorage? storage) + { + // expecting 'storageDefinition' is something like: + // | + // where can be: + // - 'user' + // - 'machine' + // where ' can be: + // - 'my' + // - 'root' + + int firstSplitIndex = storageDefinition.IndexOf(Separator); + if (firstSplitIndex != -1) + { + // there is a splitter. On the left side of it there must be . On the right there is a + + ReadOnlySpan locationSpan = storageDefinition.Slice(0, firstSplitIndex); + ReadOnlySpan nameSpan = storageDefinition.Slice(firstSplitIndex + 1); + + StoreLocation? storeLocation = null; + StoreName? storeName = null; + if (locationSpan.Equals("user", StringComparison.OrdinalIgnoreCase)) + { + storeLocation = StoreLocation.CurrentUser; + } + else if (locationSpan.Equals("machine", StringComparison.OrdinalIgnoreCase)) + { + storeLocation = StoreLocation.LocalMachine; + } + else + { + CLog.Error(string.Format("{0}: Unsupported store-location '{1}' for storage '{2}'", GetType().ToString(false), locationSpan.ToString()), typeof(CertStoreStorage).ToString(false)); + } + + if (nameSpan.Equals("my", StringComparison.OrdinalIgnoreCase)) + { + storeName = StoreName.My; + } + else if (nameSpan.Equals("root", StringComparison.OrdinalIgnoreCase)) + { + storeName = StoreName.Root; + } + else + { + CLog.Error(string.Format("{0}: Unsupported store-name '{1}' for storage '{2}'", GetType().ToString(false), nameSpan.ToString()), typeof(CertStoreStorage).ToString(false)); + } + + if (storeLocation.HasValue && storeName.HasValue) + { + storage = new CertStoreStorage(storeLocation.Value, storeName.Value); + } + else + { + storage = null; + } + } + else + { + storage = null; + } + + return storage != null; + } } +*/ \ No newline at end of file diff --git a/certmgr/Core/Converters/Impl/StorageKindConverter.cs b/certmgr/Core/Converters/Impl/StorageKindConverter.cs new file mode 100644 index 0000000..16b16ce --- /dev/null +++ b/certmgr/Core/Converters/Impl/StorageKindConverter.cs @@ -0,0 +1,47 @@ +using CertMgr.Core.Storage; + +namespace CertMgr.Core.Converters.Impl; + +internal class StorageKindConverter : ValueConverter +{ + // private const char Separator = '|'; + + protected override Task DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken) + { + ReadOnlySpan storageTypeSpan = rawValue.AsSpan(); + + // int storageTypeSplitIndex = rawSpan.IndexOf(Separator); + // if (storageTypeSplitIndex == -1) + // { + // return Task.FromResult((IStorage?)EmptyStorage.Empty); + // } + + IStorage? storage; + + // ReadOnlySpan storageTypeSpan = rawSpan.Slice(0, storageTypeSplitIndex); + // ReadOnlySpan storageDefinition = rawSpan.Slice(storageTypeSplitIndex + 1); + switch (storageTypeSpan) + { + case "file": + storage = new FileStorage(); + // if (!TryGetFileStore(storageDefinition, out storage)) + // { + // storage = EmptyStorage.Empty; + // } + break; + case "certstore": + case "cert-store": + storage = new CertStoreStorage(); + //if (!TryGetCertStore(storageDefinition, out storage)) + //{ + // storage = EmptyStorage.Empty; + //} + break; + default: + storage = EmptyStorage.Empty; + break; + } + + return Task.FromResult((IStorage?)storage); + } +} \ No newline at end of file diff --git a/certmgr/Core/Converters/Impl/TimeSpanConverter.cs b/certmgr/Core/Converters/Impl/TimeSpanConverter.cs index d814d86..af09a14 100644 --- a/certmgr/Core/Converters/Impl/TimeSpanConverter.cs +++ b/certmgr/Core/Converters/Impl/TimeSpanConverter.cs @@ -36,7 +36,7 @@ public sealed class TimeSpanConverter : ValueConverter result = TimeSpan.FromDays(value); break; case 'y': - result = TimeSpan.FromDays(value * 365); + result = TimeSpan.FromDays(value * 365 + (value / 4)); break; default: result = null; diff --git a/certmgr/Core/Jobs/JobBase.cs b/certmgr/Core/Jobs/JobBase.cs index 420f191..666bcdb 100644 --- a/certmgr/Core/Jobs/JobBase.cs +++ b/certmgr/Core/Jobs/JobBase.cs @@ -4,8 +4,11 @@ namespace CertMgr.Core.Jobs; public abstract class JobBase where TSettings : JobSettings, new() { +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. protected JobBase() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. { + // 'Settings' is set via reflection Name = JobUtils.GetJobName(GetType()); } diff --git a/certmgr/Core/PropertyDescriptor.cs b/certmgr/Core/PropertyDescriptor.cs index 4cdf0a0..7a9f42a 100644 --- a/certmgr/Core/PropertyDescriptor.cs +++ b/certmgr/Core/PropertyDescriptor.cs @@ -105,16 +105,44 @@ internal sealed class PropertyDescriptor _propertyInfo.SetValue(settings, _settingAttribute.Default); succeeded = true; } + else if (typeInfo.IsElementTypeNullable) + { + _propertyInfo.SetValue(settings, _settingAttribute.Default); + succeeded = true; + } else { CLog.Error("Default value for argument '{0}' is specified, but its type is '{1}' instead of expected '{2}'", SettingName, _settingAttribute.Default?.GetType().ToString(false) ?? "", typeInfo.ElementType.ToString(false)); } - } return succeeded; } + public AsyncResult GetDefaultValue(JobSettings settings) + { + bool success = false; + + if (_settingAttribute.Default != null) + { + Utils.TypeInfo typeInfo = TypeUtils.UnwrapCollection(_propertyInfo.PropertyType); + if (_settingAttribute.Default.GetType() == typeInfo.ElementType) + { + success = true; + } + else if (typeInfo.IsElementTypeNullable) + { + success = true; + } + else + { + CLog.Error("Default value for argument '{0}' is specified, but its type is '{1}' instead of expected '{2}'", SettingName, _settingAttribute.Default?.GetType().ToString(false) ?? "", typeInfo.ElementType.ToString(false)); + } + } + + return new AsyncResult(success, _settingAttribute.Default); + } + public override string ToString() { return string.Format("property '{0}', type = {1}, is-mandatory = {2}, validator = {3}, converter = {4}", PropertyName, PropertyTypeInfo.SourceType.ToString(false), IsMandatory ? "yes" : "no", CustomValidator?.GetType().ToString(false) ?? "", CustomConverter?.GetType().ToString(false) ?? ""); diff --git a/certmgr/Core/SettingsBuilder.cs b/certmgr/Core/SettingsBuilder.cs index 7a002e0..40f7974 100644 --- a/certmgr/Core/SettingsBuilder.cs +++ b/certmgr/Core/SettingsBuilder.cs @@ -31,24 +31,20 @@ internal sealed class SettingsBuilder foreach (PropertyDescriptor descriptor in GetPropertiesWithSettingAttribute()) { - AsyncResult setPropertyResult = await SetPropertyValueAsync(settings, descriptor, cancellationToken).ConfigureAwait(false); - if (setPropertyResult.IsSuccess) + AsyncResult result = await GetValueAsync(descriptor, settings, cancellationToken); + + if (result.IsSuccess) { - try - { - IValueValidator? validator = descriptor.CustomValidator; - if (validator != null) - { - IValidationResult valres = await validator.ValidateAsync(setPropertyResult.Value, cancellationToken).ConfigureAwait(false); - settings.ValidationResults.Add(valres); - } - } - catch (Exception e) - { - CLog.Error(e, "Failed to validate property '{0}' (of type '{1}') in settings of type '{2}'", descriptor.PropertyName, descriptor.PropertyTypeInfo.SourceType.ToString(false), _settingsType.ToString(false)); - throw new ConsoleToolsException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", descriptor.PropertyName, descriptor.PropertyTypeInfo.SourceType.ToString(false), _settingsType.ToString(false)); - } + await ValidatePropertyAsync(result.Value, descriptor, settings, cancellationToken); + + descriptor.SetValue(settings, result.Value); } + + // AsyncResult setPropertyResult = await SetPropertyValueAsync(settings, descriptor, cancellationToken).ConfigureAwait(false); + // if (setPropertyResult.IsSuccess) + // { + // await ValidatePropertyAsync(setPropertyResult.Value, descriptor, settings, cancellationToken); + // } } return settings; @@ -91,6 +87,37 @@ internal sealed class SettingsBuilder return new AsyncResult(valueSet, convertedValue); } + private async Task> GetValueAsync(PropertyDescriptor descriptor, JobSettings settings, CancellationToken cancellationToken) + { + AsyncResult result; + + if (TryGetRawArgument(descriptor, out RawArgument? rawArg)) + { + try + { + result = await ConvertRawValueAsync(descriptor, rawArg, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + CLog.Error(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", descriptor.PropertyName, descriptor.PropertyTypeInfo.SourceType.ToString(false), _settingsType.ToString(false)); + throw new ConsoleToolsException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", descriptor.PropertyName, descriptor.PropertyTypeInfo.SourceType.ToString(false), _settingsType.ToString(false)); + } + } + else if (descriptor.IsMandatory) + { + ValidationResult valres = new ValidationResult(descriptor.SettingName, false, "Mandatory argument is missing"); + settings.ValidationResults.Add(valres); + CLog.Error("mandatory argument '{0}' is missing", descriptor.SettingName); + result = new AsyncResult(false, null); + } + else + { + result = descriptor.GetDefaultValue(settings); + } + + return result; + } + private bool TryGetRawArgument(PropertyDescriptor descriptor, [NotNullWhen(true)] out RawArgument? rawArg) { rawArg = null; @@ -243,4 +270,22 @@ internal sealed class SettingsBuilder return convertedValue != null; } + + private async Task ValidatePropertyAsync(object? value, PropertyDescriptor descriptor, JobSettings settings, CancellationToken cancellationToken) + { + try + { + IValueValidator? validator = descriptor.CustomValidator; + if (validator != null) + { + IValidationResult valres = await validator.ValidateAsync(value, cancellationToken).ConfigureAwait(false); + settings.ValidationResults.Add(valres); + } + } + catch (Exception e) + { + CLog.Error(e, "Failed to validate property '{0}' (of type '{1}') in settings of type '{2}'", descriptor.PropertyName, descriptor.PropertyTypeInfo.SourceType.ToString(false), _settingsType.ToString(false)); + throw new ConsoleToolsException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", descriptor.PropertyName, descriptor.PropertyTypeInfo.SourceType.ToString(false), _settingsType.ToString(false)); + } + } } diff --git a/certmgr/Core/Storage/CertStoreLocation.cs b/certmgr/Core/Storage/CertStoreLocation.cs new file mode 100644 index 0000000..cd831e3 --- /dev/null +++ b/certmgr/Core/Storage/CertStoreLocation.cs @@ -0,0 +1,9 @@ +using System.Security.Cryptography.X509Certificates; + +namespace CertMgr.Core.Storage; + +public enum CertStoreLocation +{ + User = StoreLocation.CurrentUser, + Machine = StoreLocation.LocalMachine +} diff --git a/certmgr/Core/Storage/CertStoreName.cs b/certmgr/Core/Storage/CertStoreName.cs new file mode 100644 index 0000000..d018cd9 --- /dev/null +++ b/certmgr/Core/Storage/CertStoreName.cs @@ -0,0 +1,9 @@ +using System.Security.Cryptography.X509Certificates; + +namespace CertMgr.Core.Storage; + +public enum CertStoreName +{ + My = StoreName.My, + Root = StoreName.Root, +} diff --git a/certmgr/Core/Storage/CertStoreStorage.cs b/certmgr/Core/Storage/CertStoreStorage.cs new file mode 100644 index 0000000..9f9cc33 --- /dev/null +++ b/certmgr/Core/Storage/CertStoreStorage.cs @@ -0,0 +1,100 @@ +using System.Security.Cryptography.X509Certificates; + +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Storage; + +public sealed class CertStoreStorage : Storage +{ + private static StoreResult NotFoundResult = new StoreResult(null, false); + + protected override async Task DoReadAsync(Stream target, CertStoreStorageContext context, CancellationToken cancellationToken) + { + StoreResult result = NotFoundResult; + + using (X509Store store = OpenCertStore(context, OpenFlags.ReadWrite)) + { + foreach (X509Certificate2 cert in store.Certificates) + { + if (await context.Filter.IsMatchAsync(cert, cancellationToken)) + { + result = new StoreResult(cert, true); + break; + } + } + } + + return result; + } + + protected override async Task DoWriteAsync(Stream source, CertStoreStorageContext context, CancellationToken cancellationToken) + { + X509KeyStorageFlags flags = X509KeyStorageFlags.EphemeralKeySet; + + await using (MemoryStream ms = await CopyAsync(source, cancellationToken)) + { + using (X509Certificate2 cert = LoadCertificate(ms.GetBuffer(), context.Password, flags)) + { + using (X509Store store = OpenCertStore(context, OpenFlags.ReadWrite)) + { + AddToCertStore(store, cert); + } + } + } + + return new StoreResult(true); + } + + private async Task CopyAsync(Stream source, CancellationToken cancellationToken) + { + try + { + MemoryStream ms = new MemoryStream(); + await source.CopyToAsync(ms).ConfigureAwait(false); + return ms; + } + catch (Exception e) + { + throw new StorageException(e, "{0}: Failure while reading from stream (source stream type = '{1}')", GetType().ToString(false), source.GetType().ToString(false)); + } + } + + private X509Certificate2 LoadCertificate(byte[] data, string password, X509KeyStorageFlags flags) + { + try + { + X509Certificate2 cert = X509CertificateLoader.LoadPkcs12(data, password, flags); + return cert; + } + catch (Exception e) + { + throw new StorageException(e, "{0}: Failure while loading certificate from specified source", GetType().ToString(false)); + } + } + + private X509Store OpenCertStore(CertStoreStorageContext context, OpenFlags flags) + { + try + { + X509Store store = new X509Store((StoreName)context.Name, (StoreLocation)context.Location); + store.Open(flags); + return store; + } + catch (Exception e) + { + throw new StorageException(e, "{0}: Failure while opening cert-store (name = '{0}', location = '{1}', open-mode = '{2}'", context.Name, context.Location, flags); + } + } + + private void AddToCertStore(X509Store store, X509Certificate2 cert) + { + try + { + store.Add(cert); + } + catch (Exception e) + { + throw new StorageException(e, "{0}: Failure while adding cert to cert-store (name = '{0}', location = '{1}', cert-subj = '{2}', cert-thumbprint = '{3}'", store.Name, store.Location, cert.Subject, cert.Thumbprint); + } + } +} diff --git a/certmgr/Core/Storage/CertStoreStorageContext.cs b/certmgr/Core/Storage/CertStoreStorageContext.cs new file mode 100644 index 0000000..eb017e7 --- /dev/null +++ b/certmgr/Core/Storage/CertStoreStorageContext.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; +using System.Security.Cryptography.X509Certificates; + +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Storage; + +public sealed class CertStoreStorageContext : StorageContext +{ + public CertStoreStorageContext(CertStoreLocation storeLocation, CertStoreName storeName, X509KeyStorageFlags flags) + { + Location = storeLocation; + Name = storeName; + Flags = flags; + Filter = Filter.NotMatch; + Password = string.Empty; + } + + public CertStoreStorageContext(CertStoreLocation storeLocation, CertStoreName storeName, X509KeyStorageFlags flags, IFilter filter) + { + Location = storeLocation; + Name = storeName; + Flags = flags; + Filter = filter; + Password = string.Empty; + } + + public CertStoreLocation Location { [DebuggerStepThrough] get; } + + public CertStoreName Name { [DebuggerStepThrough] get; } + + public X509KeyStorageFlags Flags { [DebuggerStepThrough] get; } + + public IFilter Filter { [DebuggerStepThrough] get; } + + public string Password { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public override string ToString() + { + return string.Format("{0}/{1}, flags = {2}, filter-type = {3}", Location, Name, Flags, Filter?.GetType().ToString(false) ?? ""); + } +} diff --git a/certmgr/Core/Storage/CertificateFilter.cs b/certmgr/Core/Storage/CertificateFilter.cs new file mode 100644 index 0000000..4d57c2f --- /dev/null +++ b/certmgr/Core/Storage/CertificateFilter.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; +using System.Security.Cryptography.X509Certificates; + +namespace CertMgr.Core.Storage; + +public static class CertificateFilter +{ + private abstract class CertFilter : Filter + { + } + + private sealed class ByThumbprintFilter : CertFilter + { + public ByThumbprintFilter(string thumbprint) + { + Thumbprint = thumbprint; + } + + public string Thumbprint { [DebuggerStepThrough] get; } + + protected override Task DoIsMatchAsync(X509Certificate2 value, CancellationToken cancellationToken) + { + bool result = string.Equals(value.Thumbprint, Thumbprint, StringComparison.OrdinalIgnoreCase); + return Task.FromResult(result); + } + } +} diff --git a/certmgr/Core/Storage/EmptyStorage.cs b/certmgr/Core/Storage/EmptyStorage.cs index f0bf940..d39f89e 100644 --- a/certmgr/Core/Storage/EmptyStorage.cs +++ b/certmgr/Core/Storage/EmptyStorage.cs @@ -1,6 +1,6 @@ namespace CertMgr.Core.Storage; -public sealed class EmptyStorage : Storage +public sealed class EmptyStorage : Storage { public static readonly IStorage Empty = new EmptyStorage(); @@ -8,16 +8,12 @@ public sealed class EmptyStorage : Storage { } - public override bool CanWrite => true; - - public override bool CanRead => true; - - protected override Task DoWriteAsync(Stream source, CancellationToken cancellationToken) + protected override Task DoWriteAsync(Stream source, StorageContext context, CancellationToken cancellationToken) { return Task.FromResult(StoreResult.CreateSuccess()); } - protected override Task DoReadAsync(Stream target, CancellationToken cancellationToken) + protected override Task DoReadAsync(Stream target, StorageContext context, CancellationToken cancellationToken) { return Task.FromResult(StoreResult.CreateSuccess()); } diff --git a/certmgr/Core/Storage/FileStorage.cs b/certmgr/Core/Storage/FileStorage.cs index ad3a9b7..154ae8f 100644 --- a/certmgr/Core/Storage/FileStorage.cs +++ b/certmgr/Core/Storage/FileStorage.cs @@ -1,25 +1,10 @@ -using CertMgr.Core.Utils; +namespace CertMgr.Core.Storage; -namespace CertMgr.Core.Storage; - -public sealed class FileStorage : Storage +public sealed class FileStorage : Storage { - private readonly string _fileFullPath; - private readonly FileStoreMode _mode; - - public FileStorage(string fileFullPath, FileStoreMode mode) + protected override async Task DoWriteAsync(Stream source, FileStorageContext context, CancellationToken cancellationToken) { - _fileFullPath = fileFullPath; - _mode = mode; - } - - public override bool CanWrite => _mode.IsAnyOf(FileStoreMode.AppendOrNew, FileStoreMode.Create, FileStoreMode.OverwriteOrNew); - - public override bool CanRead => _mode == FileStoreMode.Open; - - protected override async Task DoWriteAsync(Stream source, CancellationToken cancellationToken) - { - using (FileStream fs = new FileStream(_fileFullPath, (FileMode)_mode, FileAccess.Write, FileShare.None)) + using (FileStream fs = new FileStream(context.Path, (FileMode)context.Mode, FileAccess.Write, FileShare.None)) { await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); } @@ -27,18 +12,13 @@ public sealed class FileStorage : Storage return StoreResult.CreateSuccess(); } - protected override async Task DoReadAsync(Stream target, CancellationToken cancellationToken) + protected override async Task DoReadAsync(Stream target, FileStorageContext context, CancellationToken cancellationToken) { - using (FileStream fs = new FileStream(_fileFullPath, (FileMode)_mode, FileAccess.Read, FileShare.Read)) + using (FileStream fs = new FileStream(context.Path, (FileMode)context.Mode, FileAccess.Read, FileShare.Read)) { await fs.CopyToAsync(target, cancellationToken).ConfigureAwait(false); } return StoreResult.CreateSuccess(); } - - public override string ToString() - { - return string.Format("{0}: mode = '{1}', path = '{2}'", GetType().Name, _mode, _fileFullPath); - } } diff --git a/certmgr/Core/Storage/FileStorageContext.cs b/certmgr/Core/Storage/FileStorageContext.cs new file mode 100644 index 0000000..f7daa1a --- /dev/null +++ b/certmgr/Core/Storage/FileStorageContext.cs @@ -0,0 +1,16 @@ +using System.Diagnostics; + +namespace CertMgr.Core.Storage; + +public sealed class FileStorageContext : StorageContext +{ + public FileStorageContext(string fileFullPath, FileStoreMode mode) + { + Path = fileFullPath; + Mode = mode; + } + + public string Path { [DebuggerStepThrough] get; } + + public FileStoreMode Mode { [DebuggerStepThrough] get; } +} diff --git a/certmgr/Core/Storage/Filter.cs b/certmgr/Core/Storage/Filter.cs new file mode 100644 index 0000000..d9156cd --- /dev/null +++ b/certmgr/Core/Storage/Filter.cs @@ -0,0 +1,38 @@ +using System.Diagnostics; + +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Storage; + +public abstract class Filter : IFilter +{ + public static readonly IFilter IsMatch = new EmptyFilter(true); + public static readonly IFilter NotMatch = new EmptyFilter(false); + + public Task IsMatchAsync(T value, CancellationToken cancellationToken) + { + return DoIsMatchAsync(value, cancellationToken); + } + + protected abstract Task DoIsMatchAsync(T value, CancellationToken cancellationToken); + + private sealed class EmptyFilter : Filter + { + internal EmptyFilter(bool result) + { + Result = result; + } + + private bool Result { [DebuggerStepThrough] get; } + + protected override Task DoIsMatchAsync(T value, CancellationToken cancellationToken) + { + return Task.FromResult(Result); + } + + public override string ToString() + { + return string.Format("{0}: is-match = {1}", GetType().ToString(false), Result ? "yes" : "no"); + } + } +} diff --git a/certmgr/Core/Storage/IFilter.cs b/certmgr/Core/Storage/IFilter.cs new file mode 100644 index 0000000..d756dae --- /dev/null +++ b/certmgr/Core/Storage/IFilter.cs @@ -0,0 +1,6 @@ +namespace CertMgr.Core.Storage; + +public interface IFilter +{ + Task IsMatchAsync(T value, CancellationToken cancellationToken); +} diff --git a/certmgr/Core/Storage/IStorage.cs b/certmgr/Core/Storage/IStorage.cs index 2869b77..30e3dd8 100644 --- a/certmgr/Core/Storage/IStorage.cs +++ b/certmgr/Core/Storage/IStorage.cs @@ -2,11 +2,7 @@ public interface IStorage { - Task WriteAsync(Stream source, CancellationToken cancellationToken); + Task WriteAsync(Stream source, StorageContext context, CancellationToken cancellationToken); - // Task WriteFromAsync(StorageAdapter adapter, CancellationToken cancellationToken); - - Task ReadAsync(Stream target, CancellationToken cancellationToken); - - // Task> ReadToAsync(StorageAdapter adapter, CancellationToken cancellationToken); + Task ReadAsync(Stream target, StorageContext context, CancellationToken cancellationToken); } diff --git a/certmgr/Core/Storage/Storage.cs b/certmgr/Core/Storage/Storage.cs index a82756a..a5c33dc 100644 --- a/certmgr/Core/Storage/Storage.cs +++ b/certmgr/Core/Storage/Storage.cs @@ -2,23 +2,15 @@ namespace CertMgr.Core.Storage; -public abstract class Storage : IStorage +public abstract class Storage : IStorage where T : StorageContext { - - public virtual bool CanRead => false; - - public virtual bool CanWrite => false; - - public async Task WriteAsync(Stream source, CancellationToken cancellationToken) + public async Task WriteAsync(Stream source, StorageContext context, CancellationToken cancellationToken) { - if (!CanWrite) - { - throw new StorageException("Cannot write. Storage not writable"); - } + T typedContext = GetTypedContext(context); try { - return await DoWriteAsync(source, cancellationToken); + return await DoWriteAsync(source, typedContext, cancellationToken); } catch (Exception e) { @@ -27,18 +19,15 @@ public abstract class Storage : IStorage } } - protected abstract Task DoWriteAsync(Stream source, CancellationToken cancellationToken); + protected abstract Task DoWriteAsync(Stream source, T context, CancellationToken cancellationToken); - public async Task ReadAsync(Stream target, CancellationToken cancellationToken) + public async Task ReadAsync(Stream target, StorageContext context, CancellationToken cancellationToken) { - if (!CanRead) - { - throw new StorageException("Cannot read. Storage not readable"); - } + T typedContext = GetTypedContext(context); try { - return await DoReadAsync(target, cancellationToken); + return await DoReadAsync(target, typedContext, cancellationToken); } catch (Exception e) { @@ -47,37 +36,26 @@ public abstract class Storage : IStorage } } - protected abstract Task DoReadAsync(Stream target, CancellationToken cancellationToken); + protected abstract Task DoReadAsync(Stream target, T context, CancellationToken cancellationToken); - /*public async Task WriteFromAsync(StorageAdapter adapter, CancellationToken cancellationToken) + private T GetTypedContext(StorageContext context) { + T typedContext; + try { - return await adapter.WriteAsync(this, cancellationToken).ConfigureAwait(false); + typedContext = (T)context; } catch (Exception e) { - StorageException ex = new StorageException(e, "Failed to write to storage of type '{0}' adapter of type '{1}'", GetType().ToString(false), adapter.GetType().ToString(false)); - return StoreResult.CreateFailure(ex); + throw new StorageException(e, "Faile to convert type {0} to {1} in storage of type {2}", context.GetType().ToString(false), typeof(T).ToString(false), GetType().ToString(false)); } - }*/ - /*public async Task> ReadToAsync(StorageAdapter adapter, CancellationToken cancellationToken) - { - try - { - TResult result = await adapter.ReadAsync(this, cancellationToken).ConfigureAwait(false); - return new StoreResult(result, true); - } - catch (Exception e) - { - StorageException ex = new StorageException(e, "Failed to read as type '{0}' from storage of type '{1}' using adapter of type '{1}'", typeof(TResult).ToString(false), GetType().ToString(false), adapter.GetType().ToString(false)); - return new StoreResult(default, false, ex); - } - }*/ + return typedContext; + } public override string ToString() { - return string.Format("{0}: can-read = {1}, can-write = {2}", GetType().Name, CanRead ? "yes" : "no", CanWrite ? "yes" : "no"); + return string.Format("{0}", GetType().ToString(false)); } } diff --git a/certmgr/Core/Storage/StorageContext.cs b/certmgr/Core/Storage/StorageContext.cs new file mode 100644 index 0000000..cd0b595 --- /dev/null +++ b/certmgr/Core/Storage/StorageContext.cs @@ -0,0 +1,5 @@ +namespace CertMgr.Core.Storage; + +public class StorageContext +{ +} diff --git a/certmgr/Core/Utils/TypeInfo.cs b/certmgr/Core/Utils/TypeInfo.cs index 3f145ab..f1be2d7 100644 --- a/certmgr/Core/Utils/TypeInfo.cs +++ b/certmgr/Core/Utils/TypeInfo.cs @@ -9,6 +9,8 @@ public sealed class TypeInfo SourceType = sourceType; IsCollection = isCollection; ElementType = elementType; + + IsElementTypeNullable = Nullable.GetUnderlyingType(ElementType) != null; } public Type SourceType { [DebuggerStepThrough] get; } @@ -17,6 +19,8 @@ public sealed class TypeInfo public Type ElementType { [DebuggerStepThrough] get; } + public bool IsElementTypeNullable { [DebuggerStepThrough] get; } + public override string ToString() { return string.Format("is-collection = {0}, source-type = '{1}', element-type = '{2}'", IsCollection ? "yes" : "no", SourceType.ToString(false), ElementType.ToString(false)); diff --git a/certmgr/Jobs/CertificateSettings.cs b/certmgr/Jobs/CertificateSettings.cs index 115604e..b54e217 100644 --- a/certmgr/Jobs/CertificateSettings.cs +++ b/certmgr/Jobs/CertificateSettings.cs @@ -6,6 +6,7 @@ using CertMgr.Core.Attributes; using CertMgr.Core.Converters.Impl; using CertMgr.Core.Jobs; using CertMgr.Core.Storage; +using CertMgr.Core.Utils; using CertMgr.Core.Validation; namespace CertMgr.Jobs; @@ -52,20 +53,102 @@ public sealed class CertificateSettings : JobSettings [Setting("is-certificate-authority", Default = false, AlternateNames = ["isca"])] public bool IsCertificateAuthority { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } - [Setting("issuer-certificate", Converter = typeof(StorageConverter))] + [Setting("issuer-kind", Converter = typeof(StorageKindConverter))] public IStorage? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + [Setting("issuer-file", Converter = typeof(FileStorageContextConverter))] + public FileStorageContext? IssuerFileContext { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("issuer-certstore", Converter = typeof(CertStoreStorageContextConverter))] + public CertStoreStorageContext? IssuerCertStoreContext { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + // [Setting("issuer-certificate", Converter = typeof(StorageConverter))] + // public IStorage? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + [Setting("issuer-password", IsSecret = true)] public string? IssuerPassword { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } - [Setting("storage", IsMandatory = true, Converter = typeof(StorageConverter))] - public IStorage? Storage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + [Setting("validity-period", Default = "1y", Converter = typeof(TimeSpanConverter))] + public TimeSpan? ValidityPeriod { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } - [Setting("password", IsSecret = true)] + // [Setting("certificate-target", IsMandatory = true, Converter = typeof(StorageConverter))] + // public IStorage? Storage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("target-password", IsSecret = true)] public string? Password { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } - [Setting("validity-period", Default = "365d", Converter = typeof(TimeSpanConverter))] - public TimeSpan? ValidityPeriod { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + [Setting("target-kind", Converter = typeof(StorageKindConverter))] + public IStorage? Target { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("target-file", Converter = typeof(FileStorageContextConverter))] + public FileStorageContext? TargetFileContext { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + [Setting("target-certstore", Converter = typeof(CertStoreStorageContextConverter))] + public CertStoreStorageContext? TargetCertStoreContext { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } + + public StorageContext GetTargetContext() + { + if (Target == null) + { + throw new JobException("Target storage is not properly configured"); + } + + StorageContext? ctx; + + switch (Target) + { + case FileStorage: + ctx = TargetFileContext; + break; + case CertStoreStorage: + if (TargetCertStoreContext != null && Password != null) + { + TargetCertStoreContext.Password = Password; + } + ctx = TargetCertStoreContext; + break; + default: + ctx = null; + break; + } + + if (ctx == null) + { + throw new JobException("context for target storage of type '{0}' is not properly set", Target.GetType().ToString(false)); + } + + return ctx; + } + + public StorageContext GetIssuerContext() + { + if (Issuer == null) + { + throw new JobException("Issuer storage is not properly configured"); + } + + StorageContext? ctx; + + switch (Issuer) + { + case FileStorage: + ctx = IssuerFileContext; + break; + case CertStoreStorage: + ctx = IssuerCertStoreContext; + break; + default: + ctx = null; + break; + } + + if (ctx == null) + { + throw new JobException("context for issuer storage of type '{0}' is not properly set", Issuer.GetType().ToString(false)); + } + + return ctx; + } protected override Task DoValidateAsync(ValidationResults results, CancellationToken cancellationToken) { @@ -92,9 +175,24 @@ public sealed class CertificateSettings : JobSettings results.AddInvalid(nameof(HashAlgorithm), "value value must be specified: '{0}'", HashAlgorithm?.ToString() ?? ""); } - if (Storage == null) + if (Target == null) { - results.AddInvalid(nameof(Storage), "must be specified"); + results.AddInvalid(nameof(Target), "must be specified"); + } + + if (Target is FileStorage) + { + if (TargetFileContext is null) + { + results.AddInvalid(nameof(TargetFileContext), "value must be specified when target is '{0}'", Target.GetType().ToString(false)); + } + } + else if (Target is CertStoreStorage) + { + if (TargetCertStoreContext is null) + { + results.AddInvalid(nameof(TargetCertStoreContext), "value must be specified when target is '{0}'", Target.GetType().ToString(false)); + } } return Task.CompletedTask; diff --git a/certmgr/Jobs/CreateCertificateJob.cs b/certmgr/Jobs/CreateCertificateJob.cs index a7b6eff..967eeee 100644 --- a/certmgr/Jobs/CreateCertificateJob.cs +++ b/certmgr/Jobs/CreateCertificateJob.cs @@ -24,18 +24,20 @@ public sealed class CreateCertificateJob : Job CertificateManager cm = new CertificateManager(); using (X509Certificate2 cert = await cm.CreateAsync(cgcs, gs, cancellationToken).ConfigureAwait(false)) { - if (Settings.Storage != null) + if (Settings.Target != null) { + StorageContext ctx = Settings.GetTargetContext(); + byte[] data = cert.Export(X509ContentType.Pfx, Settings.Password); using (MemoryStream ms = new MemoryStream()) { await ms.WriteAsync(data, cancellationToken); ms.Position = 0; - StoreResult writeResult = await Settings.Storage.WriteAsync(ms, cancellationToken).ConfigureAwait(false); + StoreResult writeResult = await Settings.Target.WriteAsync(ms, ctx, cancellationToken).ConfigureAwait(false); if (!writeResult.IsSuccess) { - throw new JobException(writeResult.Exception, "Failed to write create certificate to target storage (type = '{0}', storage = '{1}')", Settings.Storage.GetType().ToString(false), Settings.Storage.ToString()); + throw new JobException(writeResult.Exception, "Failed to write create certificate to target storage (type = '{0}', context = '{1}')", Settings.Target.GetType().ToString(false), ctx.ToString()); } } } @@ -80,9 +82,10 @@ public sealed class CreateCertificateJob : Job { using (MemoryStream ms = new MemoryStream()) { + StorageContext ctx = Settings.GetIssuerContext(); // X509KeyStorageFlags flags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet; X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet; - await Settings.Issuer.ReadAsync(ms, cancellationToken).ConfigureAwait(false); + await Settings.Issuer.ReadAsync(ms, ctx, cancellationToken).ConfigureAwait(false); cgcs.Issuer = X509CertificateLoader.LoadPkcs12(ms.GetBuffer(), Settings.IssuerPassword, flags); } } diff --git a/certmgr/Program.cs b/certmgr/Program.cs index 19a8a48..483f59a 100644 --- a/certmgr/Program.cs +++ b/certmgr/Program.cs @@ -8,16 +8,30 @@ internal static class Program { args = [ "--job=create-certificate", - "--issuer-certificate=file|o|c:\\friend2.pfx", + "--issuer-kind=file", + "--issuer-file=o|c:\\friend2.pfx", "--issuer-password=aaa", "--subject=CN=hello", "--san=world", "--san=DNS:zdrastvujte", "--san=IP:192.168.131.1", - "--algorithm=ecdsa", - "--ecdsa-curve=p384", - "--storage=file|w|c:\\mycert-ecdsa.pfx", - "--validity-period=2d" ]; + "--algorithm=ecdsa", + "--ecdsa-curve=p384", + "--validity-period=2d", + + // "--certificate-target=file|w|c:\\mycert-ecdsa.pfx", + // "--certificate-password=aaa", + + // "--target-kind=file", + // "--target-file=w|c:\\mycert-ecdsa.pfx", + // "--target-password=aaa", + + "--target-kind=certstore", + "--target-certstore=machine|my|exportable", + "--target-password=aaa" + ]; + + // --certificate-target=certstore|machine|my // args = [ // "--job=create-certificate", @@ -29,10 +43,12 @@ internal static class Program // "--san=IP:192.168.131.1", // "--algorithm=rsa", // "--rsa-key-size=2048", - // "--storage=file|w|c:\\friend-rsa.pfx", - // "--validity-period=2d" ]; + // "--validity-period=2d", + // "--certificate-target=file|w|c:\\friend-rsa.pfx", + // "--certificate-password=aaa" + // ]; - using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); + using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); JobExecutor executor = new JobExecutor(); int errorLevel = await executor.ExecuteAsync(args, cts.Token).ConfigureAwait(false); diff --git a/certmgr/certmgr.csproj b/certmgr/certmgr.csproj index dff6c33..59199ad 100644 --- a/certmgr/certmgr.csproj +++ b/certmgr/certmgr.csproj @@ -9,4 +9,8 @@ CertMgr + + + + diff --git a/certmgrTest/CertStoreStorageTest.cs b/certmgrTest/CertStoreStorageTest.cs new file mode 100644 index 0000000..ecd8673 --- /dev/null +++ b/certmgrTest/CertStoreStorageTest.cs @@ -0,0 +1,20 @@ +using CertMgr.Core.Storage; + +using NUnit.Framework; + +namespace certmgrTest; + +public class CertStoreStorageTest +{ + [Test] + public async Task First() + { + using (MemoryStream target = new MemoryStream()) + { + CertStoreStorage storage = new CertStoreStorage(); + CertStoreStorageContext context = new CertStoreStorageContext(CertStoreLocation.User, CertStoreName.My, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet); + context.Password = "aaa"; + await storage.ReadAsync(target, context, CancellationToken.None); + } + } +} diff --git a/certmgrTest/PropertyDescriptorTest.cs b/certmgrTest/PropertyDescriptorTest.cs new file mode 100644 index 0000000..9a991d4 --- /dev/null +++ b/certmgrTest/PropertyDescriptorTest.cs @@ -0,0 +1,82 @@ + +using System.Diagnostics; +using System.Reflection; + +using CertMgr.Core; +using CertMgr.Core.Attributes; +using CertMgr.Core.Jobs; +using CertMgr.Core.Validation; + +using NUnit.Framework; + +namespace certmgrTest; + +public class PropertyDescriptorTest +{ + [Test] + public void First() + { + PropertyDescriptor? descriptor = GetProperty(typeof(TestSettings), nameof(TestSettings.StringItems)); + + if (descriptor == null) + { + throw new Exception(string.Format("property not found")); + } + + IValueValidator? validator = descriptor.CustomValidator; + } + + private static PropertyDescriptor? GetProperty(Type settingsType, string name) + { + PropertyDescriptor? result = null; + + foreach (PropertyInfo propertyInfo in settingsType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (propertyInfo.Name != name) + { + continue; + } + SettingAttribute? settingAttribute = propertyInfo.GetCustomAttribute(); + if (settingAttribute is null) + { + continue; + } + + result = new PropertyDescriptor(propertyInfo, settingAttribute, settingsType); + break; + } + + return result; + } + + private sealed class TestSettings : JobSettings + { + [Setting("string-names", Validator = typeof(StringItemValidator))] + public IReadOnlyCollection? StringItems { get; set; } + } + + private sealed class StringItemValidator : IValueValidator, IValueValidator> + { + public StringItemValidator(string settingName) + { + ValueName = settingName; + } + + public string ValueName { [DebuggerStepThrough] get; } + + Task IValueValidator.ValidateAsync(string? settingValue, CancellationToken cancellationToken) + { + return Task.FromResult((IValidationResult)new ValidationResult(ValueName, true, "OK")); + } + + Task IValueValidator.ValidateAsync(object? value, CancellationToken cancellationToken) + { + return Task.FromResult((IValidationResult)new ValidationResult(ValueName, true, "OK")); + } + + Task IValueValidator>.ValidateAsync(IEnumerable? settingValue, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} diff --git a/certmgrTest/SubjectValidatorTest.cs b/certmgrTest/SubjectValidatorTest.cs index bfd3012..5e08ed2 100644 --- a/certmgrTest/SubjectValidatorTest.cs +++ b/certmgrTest/SubjectValidatorTest.cs @@ -15,7 +15,7 @@ public class SubjectValidatorTest public async Task First(string subj, bool expectedSuccess) { SubjectValidator validator = new SubjectValidator("TestSetting"); - ValidationResult result = await validator.ValidateAsync(subj, CancellationToken.None); + IValidationResult result = await validator.ValidateAsync(subj, CancellationToken.None); Assert.That(result.IsValid, Is.EqualTo(expectedSuccess), result.Justification); } }