improvements
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
125
certmgr/Core/Converters/Impl/CertStoreStorageContextConverter.cs
Normal file
125
certmgr/Core/Converters/Impl/CertStoreStorageContextConverter.cs
Normal file
@@ -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<CertStoreStorageContext>
|
||||
{
|
||||
protected override Task<CertStoreStorageContext?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||
{
|
||||
ReadOnlySpan<char> storageDefinition = rawValue.AsSpan();
|
||||
|
||||
TryGetCertStore(storageDefinition, out CertStoreStorageContext? context);
|
||||
|
||||
return Task.FromResult(context);
|
||||
}
|
||||
|
||||
private bool TryGetCertStore(ReadOnlySpan<char> storageDefinition, [NotNullWhen(true)] out CertStoreStorageContext? context)
|
||||
{
|
||||
// expecting 'storageDefinition' is something like:
|
||||
// <store-location>|<store-name>|<flags>
|
||||
// where <store-location> can be:
|
||||
// - 'user'
|
||||
// - 'machine'
|
||||
// where '<store-name> can be:
|
||||
// - 'my'
|
||||
// - 'root'
|
||||
// where <flags> 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 <store-location>. On the right there is a <store-name>
|
||||
|
||||
ReadOnlySpan<char> locationSpan = storageDefinition.Slice(0, firstSplitIndex);
|
||||
int secondSplitIndex = storageDefinition.Slice(firstSplitIndex + 1).IndexOf(Separator) + locationSpan.Length + 1;
|
||||
ReadOnlySpan<char> nameSpan = storageDefinition.Slice(firstSplitIndex + 1, secondSplitIndex - firstSplitIndex - 1);
|
||||
ReadOnlySpan<char> 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<char> enu = flagsSpan.Split(',');
|
||||
while (enu.MoveNext())
|
||||
{
|
||||
ReadOnlySpan<char> 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;
|
||||
}
|
||||
}
|
||||
99
certmgr/Core/Converters/Impl/FileStorageContextConverter.cs
Normal file
99
certmgr/Core/Converters/Impl/FileStorageContextConverter.cs
Normal file
@@ -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<FileStorageContext>
|
||||
{
|
||||
protected override Task<FileStorageContext?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||
{
|
||||
ReadOnlySpan<char> storageDefinition = rawValue.AsSpan();
|
||||
|
||||
TryGetFileStore(storageDefinition, out FileStorageContext? context);
|
||||
|
||||
return Task.FromResult(context);
|
||||
}
|
||||
|
||||
private bool TryGetFileStore(ReadOnlySpan<char> storageDefinition, [NotNullWhen(true)] out FileStorageContext? context)
|
||||
{
|
||||
// expecting that 'storageDefinition' is something like:
|
||||
// <store-mode>|<file-path>
|
||||
// or
|
||||
// |<file-path>
|
||||
// or
|
||||
// <file-path>
|
||||
//
|
||||
// where <store-mode> can be:
|
||||
// 'o' or 'overwrite' or 'overwriteornew'
|
||||
// or
|
||||
// 'a' or 'append' or 'appendornew'
|
||||
// or
|
||||
// 'c' or 'create' or 'createnew'
|
||||
//
|
||||
// where <file-path> can be:
|
||||
// 'c:\path\myfile.txt'
|
||||
// or
|
||||
// '/path/myfile.txt'
|
||||
// or
|
||||
// './path/myfile.txt'
|
||||
// in addition - <file-path> can be either quoted or double-quoted
|
||||
|
||||
context = null;
|
||||
|
||||
FileStoreMode defaultStoreMode = FileStoreMode.OverwriteOrNew;
|
||||
FileStoreMode storeMode;
|
||||
ReadOnlySpan<char> filename;
|
||||
|
||||
int firstSplitIndex = storageDefinition.IndexOf(Separator);
|
||||
if (firstSplitIndex != -1)
|
||||
{
|
||||
// there is a splitter. On the left side of it there must be <store-mode>. On the right there is a <file-path>
|
||||
|
||||
ReadOnlySpan<char> 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;
|
||||
}
|
||||
}
|
||||
8
certmgr/Core/Converters/Impl/StorageContextConverter.cs
Normal file
8
certmgr/Core/Converters/Impl/StorageContextConverter.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using CertMgr.Core.Storage;
|
||||
|
||||
namespace CertMgr.Core.Converters.Impl;
|
||||
|
||||
public abstract class StorageContextConverter<T> : ValueConverter<T> where T : StorageContext
|
||||
{
|
||||
protected const char Separator = '|';
|
||||
}
|
||||
@@ -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<IStorage>
|
||||
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<IStorage>
|
||||
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<IStorage>
|
||||
storage = new FileStorage(filename.ToString(), storeMode);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetCertStore(ReadOnlySpan<char> storageDefinition, [NotNullWhen(true)] out IStorage? storage)
|
||||
{
|
||||
// expecting 'storageDefinition' is something like:
|
||||
// <store-location>|<store-name>
|
||||
// where <store-location> can be:
|
||||
// - 'user'
|
||||
// - 'machine'
|
||||
// where '<store-name> 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 <store-location>. On the right there is a <store-name>
|
||||
|
||||
ReadOnlySpan<char> locationSpan = storageDefinition.Slice(0, firstSplitIndex);
|
||||
ReadOnlySpan<char> 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;
|
||||
}
|
||||
}
|
||||
*/
|
||||
47
certmgr/Core/Converters/Impl/StorageKindConverter.cs
Normal file
47
certmgr/Core/Converters/Impl/StorageKindConverter.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using CertMgr.Core.Storage;
|
||||
|
||||
namespace CertMgr.Core.Converters.Impl;
|
||||
|
||||
internal class StorageKindConverter : ValueConverter<IStorage>
|
||||
{
|
||||
// private const char Separator = '|';
|
||||
|
||||
protected override Task<IStorage?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||
{
|
||||
ReadOnlySpan<char> storageTypeSpan = rawValue.AsSpan();
|
||||
|
||||
// int storageTypeSplitIndex = rawSpan.IndexOf(Separator);
|
||||
// if (storageTypeSplitIndex == -1)
|
||||
// {
|
||||
// return Task.FromResult((IStorage?)EmptyStorage.Empty);
|
||||
// }
|
||||
|
||||
IStorage? storage;
|
||||
|
||||
// ReadOnlySpan<char> storageTypeSpan = rawSpan.Slice(0, storageTypeSplitIndex);
|
||||
// ReadOnlySpan<char> 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);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public sealed class TimeSpanConverter : ValueConverter<TimeSpan?>
|
||||
result = TimeSpan.FromDays(value);
|
||||
break;
|
||||
case 'y':
|
||||
result = TimeSpan.FromDays(value * 365);
|
||||
result = TimeSpan.FromDays(value * 365 + (value / 4));
|
||||
break;
|
||||
default:
|
||||
result = null;
|
||||
|
||||
@@ -4,8 +4,11 @@ namespace CertMgr.Core.Jobs;
|
||||
|
||||
public abstract class JobBase<TSettings> 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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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) ?? "<null>", typeInfo.ElementType.ToString(false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
public AsyncResult<object?> 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) ?? "<null>", typeInfo.ElementType.ToString(false));
|
||||
}
|
||||
}
|
||||
|
||||
return new AsyncResult<object?>(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) ?? "<null>", CustomConverter?.GetType().ToString(false) ?? "<null>");
|
||||
|
||||
@@ -31,24 +31,20 @@ internal sealed class SettingsBuilder
|
||||
|
||||
foreach (PropertyDescriptor descriptor in GetPropertiesWithSettingAttribute())
|
||||
{
|
||||
AsyncResult<object?> setPropertyResult = await SetPropertyValueAsync(settings, descriptor, cancellationToken).ConfigureAwait(false);
|
||||
if (setPropertyResult.IsSuccess)
|
||||
AsyncResult<object?> 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<object?> 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<object?>(valueSet, convertedValue);
|
||||
}
|
||||
|
||||
private async Task<AsyncResult<object?>> GetValueAsync(PropertyDescriptor descriptor, JobSettings settings, CancellationToken cancellationToken)
|
||||
{
|
||||
AsyncResult<object?> 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<object?>(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
certmgr/Core/Storage/CertStoreLocation.cs
Normal file
9
certmgr/Core/Storage/CertStoreLocation.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace CertMgr.Core.Storage;
|
||||
|
||||
public enum CertStoreLocation
|
||||
{
|
||||
User = StoreLocation.CurrentUser,
|
||||
Machine = StoreLocation.LocalMachine
|
||||
}
|
||||
9
certmgr/Core/Storage/CertStoreName.cs
Normal file
9
certmgr/Core/Storage/CertStoreName.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace CertMgr.Core.Storage;
|
||||
|
||||
public enum CertStoreName
|
||||
{
|
||||
My = StoreName.My,
|
||||
Root = StoreName.Root,
|
||||
}
|
||||
100
certmgr/Core/Storage/CertStoreStorage.cs
Normal file
100
certmgr/Core/Storage/CertStoreStorage.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
using CertMgr.Core.Utils;
|
||||
|
||||
namespace CertMgr.Core.Storage;
|
||||
|
||||
public sealed class CertStoreStorage : Storage<CertStoreStorageContext>
|
||||
{
|
||||
private static StoreResult<X509Certificate2> NotFoundResult = new StoreResult<X509Certificate2>(null, false);
|
||||
|
||||
protected override async Task<StoreResult> DoReadAsync(Stream target, CertStoreStorageContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
StoreResult<X509Certificate2> 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<X509Certificate2>(cert, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override async Task<StoreResult> 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<MemoryStream> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
certmgr/Core/Storage/CertStoreStorageContext.cs
Normal file
42
certmgr/Core/Storage/CertStoreStorageContext.cs
Normal file
@@ -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<X509Certificate2>.NotMatch;
|
||||
Password = string.Empty;
|
||||
}
|
||||
|
||||
public CertStoreStorageContext(CertStoreLocation storeLocation, CertStoreName storeName, X509KeyStorageFlags flags, IFilter<X509Certificate2> 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<X509Certificate2> 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) ?? "<null>");
|
||||
}
|
||||
}
|
||||
27
certmgr/Core/Storage/CertificateFilter.cs
Normal file
27
certmgr/Core/Storage/CertificateFilter.cs
Normal file
@@ -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<X509Certificate2>
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class ByThumbprintFilter : CertFilter
|
||||
{
|
||||
public ByThumbprintFilter(string thumbprint)
|
||||
{
|
||||
Thumbprint = thumbprint;
|
||||
}
|
||||
|
||||
public string Thumbprint { [DebuggerStepThrough] get; }
|
||||
|
||||
protected override Task<bool> DoIsMatchAsync(X509Certificate2 value, CancellationToken cancellationToken)
|
||||
{
|
||||
bool result = string.Equals(value.Thumbprint, Thumbprint, StringComparison.OrdinalIgnoreCase);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace CertMgr.Core.Storage;
|
||||
|
||||
public sealed class EmptyStorage : Storage
|
||||
public sealed class EmptyStorage : Storage<StorageContext>
|
||||
{
|
||||
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<StoreResult> DoWriteAsync(Stream source, CancellationToken cancellationToken)
|
||||
protected override Task<StoreResult> DoWriteAsync(Stream source, StorageContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(StoreResult.CreateSuccess());
|
||||
}
|
||||
|
||||
protected override Task<StoreResult> DoReadAsync(Stream target, CancellationToken cancellationToken)
|
||||
protected override Task<StoreResult> DoReadAsync(Stream target, StorageContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(StoreResult.CreateSuccess());
|
||||
}
|
||||
|
||||
@@ -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<FileStorageContext>
|
||||
{
|
||||
private readonly string _fileFullPath;
|
||||
private readonly FileStoreMode _mode;
|
||||
|
||||
public FileStorage(string fileFullPath, FileStoreMode mode)
|
||||
protected override async Task<StoreResult> 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<StoreResult> 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<StoreResult> DoReadAsync(Stream target, CancellationToken cancellationToken)
|
||||
protected override async Task<StoreResult> 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);
|
||||
}
|
||||
}
|
||||
|
||||
16
certmgr/Core/Storage/FileStorageContext.cs
Normal file
16
certmgr/Core/Storage/FileStorageContext.cs
Normal file
@@ -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; }
|
||||
}
|
||||
38
certmgr/Core/Storage/Filter.cs
Normal file
38
certmgr/Core/Storage/Filter.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using CertMgr.Core.Utils;
|
||||
|
||||
namespace CertMgr.Core.Storage;
|
||||
|
||||
public abstract class Filter<T> : IFilter<T>
|
||||
{
|
||||
public static readonly IFilter<T> IsMatch = new EmptyFilter(true);
|
||||
public static readonly IFilter<T> NotMatch = new EmptyFilter(false);
|
||||
|
||||
public Task<bool> IsMatchAsync(T value, CancellationToken cancellationToken)
|
||||
{
|
||||
return DoIsMatchAsync(value, cancellationToken);
|
||||
}
|
||||
|
||||
protected abstract Task<bool> DoIsMatchAsync(T value, CancellationToken cancellationToken);
|
||||
|
||||
private sealed class EmptyFilter : Filter<T>
|
||||
{
|
||||
internal EmptyFilter(bool result)
|
||||
{
|
||||
Result = result;
|
||||
}
|
||||
|
||||
private bool Result { [DebuggerStepThrough] get; }
|
||||
|
||||
protected override Task<bool> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
6
certmgr/Core/Storage/IFilter.cs
Normal file
6
certmgr/Core/Storage/IFilter.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace CertMgr.Core.Storage;
|
||||
|
||||
public interface IFilter<T>
|
||||
{
|
||||
Task<bool> IsMatchAsync(T value, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
public interface IStorage
|
||||
{
|
||||
Task<StoreResult> WriteAsync(Stream source, CancellationToken cancellationToken);
|
||||
Task<StoreResult> WriteAsync(Stream source, StorageContext context, CancellationToken cancellationToken);
|
||||
|
||||
// Task<StoreResult> WriteFromAsync<TResult>(StorageAdapter<TResult> adapter, CancellationToken cancellationToken);
|
||||
|
||||
Task<StoreResult> ReadAsync(Stream target, CancellationToken cancellationToken);
|
||||
|
||||
// Task<StoreResult<T>> ReadToAsync<T>(StorageAdapter<T> adapter, CancellationToken cancellationToken);
|
||||
Task<StoreResult> ReadAsync(Stream target, StorageContext context, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -2,23 +2,15 @@
|
||||
|
||||
namespace CertMgr.Core.Storage;
|
||||
|
||||
public abstract class Storage : IStorage
|
||||
public abstract class Storage<T> : IStorage where T : StorageContext
|
||||
{
|
||||
|
||||
public virtual bool CanRead => false;
|
||||
|
||||
public virtual bool CanWrite => false;
|
||||
|
||||
public async Task<StoreResult> WriteAsync(Stream source, CancellationToken cancellationToken)
|
||||
public async Task<StoreResult> 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<StoreResult> DoWriteAsync(Stream source, CancellationToken cancellationToken);
|
||||
protected abstract Task<StoreResult> DoWriteAsync(Stream source, T context, CancellationToken cancellationToken);
|
||||
|
||||
public async Task<StoreResult> ReadAsync(Stream target, CancellationToken cancellationToken)
|
||||
public async Task<StoreResult> 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<StoreResult> DoReadAsync(Stream target, CancellationToken cancellationToken);
|
||||
protected abstract Task<StoreResult> DoReadAsync(Stream target, T context, CancellationToken cancellationToken);
|
||||
|
||||
/*public async Task<StoreResult> WriteFromAsync<TResult>(StorageAdapter<TResult> 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<StoreResult<TResult>> ReadToAsync<TResult>(StorageAdapter<TResult> adapter, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
TResult result = await adapter.ReadAsync(this, cancellationToken).ConfigureAwait(false);
|
||||
return new StoreResult<TResult>(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<TResult>(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));
|
||||
}
|
||||
}
|
||||
|
||||
5
certmgr/Core/Storage/StorageContext.cs
Normal file
5
certmgr/Core/Storage/StorageContext.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace CertMgr.Core.Storage;
|
||||
|
||||
public class StorageContext
|
||||
{
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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() ?? "<null>");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -24,18 +24,20 @@ public sealed class CreateCertificateJob : Job<CertificateSettings>
|
||||
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<CertificateSettings>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -9,4 +9,8 @@
|
||||
<RootNamespace>CertMgr</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Core\Storage\Filters\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
20
certmgrTest/CertStoreStorageTest.cs
Normal file
20
certmgrTest/CertStoreStorageTest.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
certmgrTest/PropertyDescriptorTest.cs
Normal file
82
certmgrTest/PropertyDescriptorTest.cs
Normal file
@@ -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<SettingAttribute>();
|
||||
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<string>? StringItems { get; set; }
|
||||
}
|
||||
|
||||
private sealed class StringItemValidator : IValueValidator<string>, IValueValidator<IEnumerable<string>>
|
||||
{
|
||||
public StringItemValidator(string settingName)
|
||||
{
|
||||
ValueName = settingName;
|
||||
}
|
||||
|
||||
public string ValueName { [DebuggerStepThrough] get; }
|
||||
|
||||
Task<IValidationResult> IValueValidator<string>.ValidateAsync(string? settingValue, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult((IValidationResult)new ValidationResult(ValueName, true, "OK"));
|
||||
}
|
||||
|
||||
Task<IValidationResult> IValueValidator.ValidateAsync(object? value, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult((IValidationResult)new ValidationResult(ValueName, true, "OK"));
|
||||
}
|
||||
|
||||
Task<IValidationResult> IValueValidator<IEnumerable<string>>.ValidateAsync(IEnumerable<string>? settingValue, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user