initial commit

This commit is contained in:
2025-10-18 10:26:43 +02:00
commit 43fba99802
87 changed files with 3696 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.vs
certmgr/obj/*
certmgr/Example.cs
certmgr/Obsolete/*

View File

@@ -0,0 +1,23 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v9.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v9.0": {
"certmgr/1.0.0": {
"runtime": {
"certmgr.dll": {}
}
}
}
},
"libraries": {
"certmgr/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,12 @@
{
"runtimeOptions": {
"tfm": "net9.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "9.0.0"
},
"configProperties": {
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

25
certmgr.sln Normal file
View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36511.14 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "certmgr", "certmgr\certmgr.csproj", "{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AFA87038-4B40-4962-A801-DA2E96351533}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,17 @@
namespace CertMgr.CertGen;
public class CertGenException : Exception
{
public CertGenException(string message)
: base(message)
{
}
public CertGenException(string message, params object?[] args)
: base(string.Format(message, args))
{
}
public CertGenException(Exception innerException, string message)
: base(message, innerException)
{
}
}

View File

@@ -0,0 +1,7 @@
namespace CertMgr.CertGen;
public enum CertificateAlgorithm
{
RSA = 1,
ECDsa
}

View File

@@ -0,0 +1,238 @@
using System.Diagnostics;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using CertMgr.Core.Exceptions;
namespace CertMgr.CertGen;
internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICertificateGenerator
where TAlgorithm : AsymmetricAlgorithm
where TSettings : GeneratorSettings
{
internal CertificateGeneratorBase(TSettings settings)
{
Settings = settings;
}
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
}
protected TSettings Settings { [DebuggerStepThrough] get; }
public async Task<X509Certificate2> CreateAsync(CertificateSettings settings, CancellationToken cancellationToken)
{
ValidateSettings(settings);
X509Certificate2 cert = await CreateInternalAsync(settings, cancellationToken);
return cert;
}
protected abstract bool IsEphemeral(TAlgorithm key);
protected virtual void ValidateSettings(CertificateSettings settings)
{
if (settings.Issuer != null)
{
if (!settings.Issuer.HasPrivateKey)
{
throw new CertGenException("Certificate without private key cannot be used for signing");
}
}
}
protected HashAlgorithmName GetHashAlgorithm()
{
HashAlgorithmName han;
switch (Settings.HashAlgorithm)
{
case HashAlgorithm.Sha256:
han = HashAlgorithmName.SHA256;
break;
case HashAlgorithm.Sha384:
han = HashAlgorithmName.SHA384;
break;
case HashAlgorithm.Sha512:
han = HashAlgorithmName.SHA512;
break;
default:
throw new UnsupportedValueException(Settings.HashAlgorithm);
}
return han;
}
protected abstract TAlgorithm CreatePrivateKey();
protected abstract CertificateRequest DoCreateRequest(string subjectName, TAlgorithm privateKey);
protected abstract X509Certificate2 JoinPrivateKey(X509Certificate2 publicOnlyCert, TAlgorithm privateKey);
protected abstract string? GetContainerUniqueName(TAlgorithm privateKey);
protected abstract TAlgorithm? GetPrivateKey(X509Certificate2 cert);
private CertificateRequest CreateRequest(CertificateSettings settings, TAlgorithm privateKey)
{
string commonName = CreateCommonName(settings.SubjectName);
CertificateRequest request = DoCreateRequest(commonName, privateKey);
SubjectAlternativeNameBuilder altNames = new SubjectAlternativeNameBuilder();
foreach (string subjName in settings.SubjectAlternateNames)
{
altNames.AddDnsName(subjName);
}
request.CertificateExtensions.Add(altNames.Build());
if (settings.IsCertificateAuthority)
{
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(settings.IsCertificateAuthority, false, 0, false));
}
if (settings.KeyUsage != X509KeyUsageFlags.None)
{
request.CertificateExtensions.Add(new X509KeyUsageExtension(settings.KeyUsage, false));
}
return request;
}
private Task<X509Certificate2> CreateInternalAsync(CertificateSettings settings, CancellationToken cancellationToken)
{
X509Certificate2 cert;
using (TAlgorithm privateKey = CreatePrivateKey())
{
CertificateRequest request = CreateRequest(settings, privateKey);
DateTimeOffset notBefore = DateTimeOffset.UtcNow.AddSeconds(-1);
DateTimeOffset notAfter = DateTimeOffset.UtcNow.Add(settings.ValidityPeriod);
if (settings.Issuer != null)
{
byte[] serial = GenerateSerial();
X509SignatureGenerator sgen = GetSignatureGenerator(settings.Issuer);
using (X509Certificate2 publicOnlyCert = request.Create(settings.Issuer.SubjectName, sgen, notBefore, notAfter, serial))
{
cert = JoinPrivateKey(publicOnlyCert, privateKey);
// using (X509Certificate2 temp = JoinPrivateKey(publicOnlyCert, privateKey))
// {
// // Generated instance of the cert can't be added to cert-store (private key is missing) if requested by the caller.
// // To avoid this recreate the cert
// cert = Recreate(temp, settings);
// }
}
}
else
{
cert = request.CreateSelfSigned(notBefore, notAfter);
// using (X509Certificate2 temp = request.CreateSelfSigned(notBefore, notAfter))
// {
// // Generated instance of the cert can't be added to cert-store (private key is missing) if requested by the caller.
// // To avoid this recreate the cert
// cert = Recreate(temp, settings);
// }
}
}
return Task.FromResult(cert);
}
private X509SignatureGenerator GetSignatureGenerator(X509Certificate2 issuerCertificate)
{
X509SignatureGenerator? sgen = null;
ECDsa? ecdsa = issuerCertificate.GetECDsaPrivateKey();
if (ecdsa != null)
{
sgen = X509SignatureGenerator.CreateForECDsa(ecdsa);
}
if (sgen == null)
{
RSA? rsa = issuerCertificate.GetRSAPrivateKey();
if (rsa != null)
{
sgen = X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1);
}
}
if (sgen == null)
{
DSA? dsa = issuerCertificate.GetDSAPrivateKey();
if (dsa != null)
{
throw new CertGenException("Unsupported type of DSA private-key: '{0}'", dsa.GetType().FullName);
}
else
{
try
{
AsymmetricAlgorithm pk = issuerCertificate.PrivateKey;
throw new CertGenException("Unsupported type of private-key: '{0}'", pk?.GetType().FullName ?? "<null>");
}
catch (CertGenException)
{
throw;
}
catch
{
throw new CertGenException("Unsupported type of private-key");
}
throw new CertGenException("Unsupported type of private-key (no DSA or CAPI)");
}
}
return sgen;
}
private static byte[] GenerateSerial()
{
byte[] serial = new byte[12];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(serial);
}
return serial;
}
private X509Certificate2 Recreate(X509Certificate2 source, CertificateSettings settings)
{
byte[] data = source.Export(X509ContentType.Pfx, string.Empty);
X509KeyStorageFlags flags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet;
if (!settings.ExportableKeys)
{
flags &= ~X509KeyStorageFlags.Exportable;
}
X509Certificate2 target = X509CertificateLoader.LoadPkcs12(data, string.Empty);
target.FriendlyName = settings.FriendlyName;
return target;
}
private string CreateCommonName(string? subjectName)
{
string cn;
if (subjectName.StartsWith("CN=", StringComparison.OrdinalIgnoreCase))
{
cn = subjectName;
}
else
{
cn = string.Format("CN={0}", subjectName);
}
return cn;
}
public override string ToString()
{
return string.Format("Type = {0}", this is RsaCertificateGenerator ? "RSA" : this is EcdsaCertificateGenerator ? "ECDSA" : "<unknown>");
}
}

View File

@@ -0,0 +1,29 @@
using System.Security.Cryptography.X509Certificates;
using CertMgr.Core.Exceptions;
namespace CertMgr.CertGen;
public sealed class CertificateManager
{
public async Task<X509Certificate2> CreateAsync(CertificateSettings cs, GeneratorSettings gs, CancellationToken cancellationToken)
{
ICertificateGenerator generator;
if (gs is EcdsaGeneratorSettings egs)
{
generator = new EcdsaCertificateGenerator(egs);
}
else if (gs is RsaGeneratorSettings rgs)
{
generator = new RsaCertificateGenerator(rgs);
}
else
{
throw new UnsupportedValueException(gs.Type);
}
X509Certificate2 cert = await generator.CreateAsync(cs, cancellationToken);
return cert;
}
}

View File

@@ -0,0 +1,61 @@
using System.Diagnostics;
using System.Security.Cryptography.X509Certificates;
using CertMgr.Core.Utils;
using CertMgr.Core.Validation;
namespace CertMgr.CertGen;
public sealed class CertificateSettings
{
internal CertificateSettings()
{
SubjectAlternateNames = new SubjectAlternateNames([NetUtils.MachineName]);
SubjectName = NetUtils.MachineName;
Issuer = null;
FriendlyName = NetUtils.MachineName;
ValidityPeriod = TimeSpan.FromDays(3650 + 2);
ExportableKeys = true;
IsCertificateAuthority = false;
KeyUsage = X509KeyUsageFlags.None;
}
public SubjectAlternateNames SubjectAlternateNames { [DebuggerStepThrough] get; }
public string? SubjectName { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public X509Certificate2? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public string? FriendlyName { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public TimeSpan ValidityPeriod { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public bool ExportableKeys { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public bool IsCertificateAuthority { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public X509KeyUsageFlags KeyUsage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public Task<ValidationResults> ValidateAsync(CancellationToken cancellationToken)
{
ValidationResults results = new ValidationResults();
if (string.IsNullOrEmpty(SubjectName))
{
results.AddInvalid(nameof(SubjectName), "must not be empty");
}
if (ValidityPeriod < TimeSpan.FromSeconds(1))
{
results.AddInvalid(nameof(ValidityPeriod), "must be greater than 1sec");
}
return Task.FromResult(results);
}
public override string ToString()
{
return string.Format("issuer = {0}, subject-name = '{1}', validity-period = {2} days, exportable = {3}, is-ca = {4}", Issuer?.Subject ?? "<not-set>", SubjectAlternateNames.First(), ValidityPeriod.TotalDays, ExportableKeys ? "yes" : "no", IsCertificateAuthority ? "yes" : "no");
}
}

View File

@@ -0,0 +1,67 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using CertMgr.Core.Exceptions;
namespace CertMgr.CertGen;
internal sealed class EcdsaCertificateGenerator : CertificateGeneratorBase<ECDsa, EcdsaGeneratorSettings>
{
internal EcdsaCertificateGenerator(EcdsaGeneratorSettings settings)
: base(settings)
{
}
protected override ECDsa CreatePrivateKey()
{
return ECDsa.Create(GetCurve());
}
protected override CertificateRequest DoCreateRequest(string subjectName, ECDsa privateKey)
{
return new CertificateRequest(subjectName, privateKey, GetHashAlgorithm());
}
protected override X509Certificate2 JoinPrivateKey(X509Certificate2 publicOnlyCert, ECDsa privateKey)
{
return publicOnlyCert.CopyWithPrivateKey(privateKey);
}
protected override ECDsa? GetPrivateKey(X509Certificate2 cert)
{
ECDsa? privateKey = cert.GetECDsaPrivateKey();
return privateKey;
}
protected override string? GetContainerUniqueName(ECDsa privateKey)
{
return ((ECDsaCng)privateKey).Key.UniqueName;
}
protected override bool IsEphemeral(ECDsa key)
{
return ((ECDsaCng)key).Key.IsEphemeral;
}
private ECCurve GetCurve()
{
ECCurve curve;
switch (Settings.Curve)
{
case EcdsaCurve.P256:
curve = ECCurve.NamedCurves.nistP256;
break;
case EcdsaCurve.P384:
curve = ECCurve.NamedCurves.nistP384;
break;
case EcdsaCurve.P521:
curve = ECCurve.NamedCurves.nistP521;
break;
default:
throw new UnsupportedValueException(Settings.Curve);
}
return curve;
}
}

View File

@@ -0,0 +1,8 @@
namespace CertMgr.CertGen;
public enum EcdsaCurve
{
P256 = 1,
P384,
P521
}

View File

@@ -0,0 +1,44 @@
using System.Diagnostics;
using CertMgr.Core.Exceptions;
namespace CertMgr.CertGen;
public sealed class EcdsaGeneratorSettings : GeneratorSettings
{
public EcdsaGeneratorSettings(EcdsaCurve curve)
: base(GeneratorType.Ecdsa)
{
Curve = curve;
HashAlgorithm = GetHashAlgorithm(curve);
}
public EcdsaCurve Curve { [DebuggerStepThrough] get; }
private HashAlgorithm GetHashAlgorithm(EcdsaCurve curve)
{
HashAlgorithm ha;
switch (Curve)
{
case EcdsaCurve.P256:
ha = HashAlgorithm.Sha256;
break;
case EcdsaCurve.P384:
ha = HashAlgorithm.Sha384;
break;
case EcdsaCurve.P521:
ha = HashAlgorithm.Sha512;
break;
default:
throw new UnsupportedValueException(Curve);
}
return ha;
}
public override string ToString()
{
return string.Format("type = '{0}', curve = '{1}', hash-algorithm = '{2}'", Type, Curve, HashAlgorithm);
}
}

View File

@@ -0,0 +1,20 @@
using System.Diagnostics;
namespace CertMgr.CertGen;
public abstract class GeneratorSettings
{
protected GeneratorSettings(GeneratorType type)
{
Type = type;
}
public GeneratorType Type { [DebuggerStepThrough] get; }
public HashAlgorithm HashAlgorithm { [DebuggerStepThrough] get; [DebuggerStepThrough] protected set; }
public override string ToString()
{
return string.Format("type = '{0}', hash-algorithm = '{1}'", Type, HashAlgorithm);
}
}

View File

@@ -0,0 +1,7 @@
namespace CertMgr.CertGen;
public enum GeneratorType
{
Ecdsa = 1,
Rsa
}

View File

@@ -0,0 +1,8 @@
namespace CertMgr.CertGen;
public enum HashAlgorithm
{
Sha256 = 1,
Sha384,
Sha512
}

View File

@@ -0,0 +1,8 @@
using System.Security.Cryptography.X509Certificates;
namespace CertMgr.CertGen;
public interface ICertificateGenerator : IAsyncDisposable
{
Task<X509Certificate2> CreateAsync(CertificateSettings settings, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,67 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using CertMgr.Core.Exceptions;
namespace CertMgr.CertGen;
internal sealed class RsaCertificateGenerator : CertificateGeneratorBase<RSA, RsaGeneratorSettings>
{
internal RsaCertificateGenerator(RsaGeneratorSettings settings)
: base(settings)
{
}
protected override RSA CreatePrivateKey()
{
return RSA.Create(GetKeySize());
}
protected override CertificateRequest DoCreateRequest(string subjectName, RSA privateKey)
{
return new CertificateRequest(subjectName, privateKey, GetHashAlgorithm(), RSASignaturePadding.Pkcs1);
}
protected override X509Certificate2 JoinPrivateKey(X509Certificate2 publicOnlyCert, RSA privateKey)
{
return publicOnlyCert.CopyWithPrivateKey(privateKey);
}
protected override RSA? GetPrivateKey(X509Certificate2 cert)
{
RSA? privateKey = cert.GetRSAPrivateKey();
return privateKey;
}
protected override string? GetContainerUniqueName(RSA privateKey)
{
return ((RSACng)privateKey).Key.UniqueName;
}
protected override bool IsEphemeral(RSA key)
{
return ((RSACng)key).Key.IsEphemeral;
}
private int GetKeySize()
{
int keySize;
switch (Settings.KeySize)
{
case RsaKeySize.KeySize2048:
keySize = 2048;
break;
case RsaKeySize.KeySize4096:
keySize = 4096;
break;
case RsaKeySize.KeySize8192:
keySize = 8192;
break;
default:
throw new UnsupportedValueException(Settings.KeySize);
}
return keySize;
}
}

View File

@@ -0,0 +1,45 @@
using System.Diagnostics;
using CertMgr.Core.Exceptions;
namespace CertMgr.CertGen;
public sealed class RsaGeneratorSettings : GeneratorSettings
{
public RsaGeneratorSettings(RsaKeySize keySize)
: base(GeneratorType.Rsa)
{
KeySize = keySize;
HashAlgorithm = GetHashAlgorithm(keySize);
}
public RsaKeySize KeySize { [DebuggerStepThrough] get; }
private HashAlgorithm GetHashAlgorithm(RsaKeySize keySize)
{
HashAlgorithm ha;
switch (keySize)
{
case RsaKeySize.KeySize2048:
ha = HashAlgorithm.Sha256;
break;
case RsaKeySize.KeySize4096:
ha = HashAlgorithm.Sha384;
break;
case RsaKeySize.KeySize8192:
ha = HashAlgorithm.Sha512;
break;
default:
throw new UnsupportedValueException(keySize);
}
return ha;
}
public override string ToString()
{
return string.Format("type = '{0}', key-size = '{1}', hash-algorithm = '{2}'", Type, KeySize, HashAlgorithm);
}
}

View File

@@ -0,0 +1,8 @@
namespace CertMgr.CertGen;
public enum RsaKeySize
{
KeySize2048 = 1,
KeySize4096,
KeySize8192
}

View File

@@ -0,0 +1,41 @@
using System.Collections;
namespace CertMgr.CertGen;
public sealed class SubjectAlternateNames : IReadOnlyCollection<string>
{
private readonly HashSet<string> _items;
public SubjectAlternateNames()
{
_items = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
public SubjectAlternateNames(IReadOnlyCollection<string> items)
{
_items = new HashSet<string>(items, StringComparer.OrdinalIgnoreCase);
}
public int Count => _items.Count;
public bool Add(string name)
{
bool added = _items.Add(name);
return added;
}
public IEnumerator<string> GetEnumerator()
{
return _items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public override string ToString()
{
return string.Format("count = {0}", _items.Count);
}
}

View File

@@ -0,0 +1,26 @@
using CertMgr.Core.Utils;
namespace CertMgr.CertGen.Utils;
public sealed class CollectionEquivalencyComparer<T> : IEqualityComparer<IEnumerable<T>>
{
public bool Equals(IEnumerable<T>? x, IEnumerable<T>? y)
{
if (x == y)
{
return true;
}
if (x == null || y == null)
{
return false;
}
bool equivalent = x.Equivalent(y);
return equivalent;
}
public int GetHashCode(IEnumerable<T> obj)
{
return obj?.Sum(it => it?.GetHashCode() ?? 0) ?? 0;
}
}

View File

@@ -0,0 +1,49 @@
/*using System.Security.Cryptography.X509Certificates;
using CertMgr.Core.Storage;
namespace CertMgr.CertGen.Utils;
public sealed class StorageToX509CertificateAdapter : StorageAdapter<X509Certificate2>
{
private readonly X509Certificate2? _cert;
private readonly string _password;
private readonly X509KeyStorageFlags? _flags;
public StorageToX509CertificateAdapter(X509Certificate2 cert, string? password)
{
_cert = cert;
_password = password ?? string.Empty;
}
public StorageToX509CertificateAdapter(string? password, X509KeyStorageFlags flags)
{
_password = password ?? string.Empty;
_flags = flags;
}
protected override async Task<X509Certificate2> DoReadAsync(IStorage source, CancellationToken cancellationToken)
{
X509Certificate2 cert;
using (MemoryStream ms = new MemoryStream())
{
await source.ReadAsync(ms, cancellationToken);
cert = X509CertificateLoader.LoadPkcs12(ms.GetBuffer(), _password, _flags.Value);
}
return cert;
}
protected override async Task<StoreResult> DoWriteAsync(IStorage target, CancellationToken cancellationToken)
{
byte[] data = _cert.Export(X509ContentType.Pfx, _password);
using (MemoryStream ms = new MemoryStream())
{
await ms.WriteAsync(data, cancellationToken);
ms.Position = 0;
return await target.WriteAsync(ms, cancellationToken).ConfigureAwait(false);
}
}
}
*/

View File

@@ -0,0 +1,29 @@
using System.Diagnostics;
namespace CertMgr.Core.Attributes;
public sealed class SettingAttribute : Attribute
{
public SettingAttribute(string name)
{
Name = name;
IsMandatory = false;
}
public string Name { [DebuggerStepThrough] get; }
public string[] AlternateNames { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public bool IsMandatory { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public Type? Validator { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public Type? Converter { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public object? Default { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public override string ToString()
{
return string.Format("name = '{0}', is-mandatory = {1}, validator = {2}, converter = {3}", Name, IsMandatory ? "yes" : "no", Validator?.GetType().Name ?? "<not-set>", Converter?.GetType().Name ?? "<not-set>");
}
}

View File

@@ -0,0 +1,15 @@
using CertMgr.Core.Utils;
namespace CertMgr.Core;
public class CertMgrException : Exception
{
internal CertMgrException(string messageFormat, params object[] messageArgs)
: base(StringFormatter.Format(messageFormat, messageArgs))
{
}
internal CertMgrException(Exception innerException, string messageFormat, params object[] messageArgs)
: base(StringFormatter.Format(messageFormat, messageArgs), innerException)
{
}
}

View File

@@ -0,0 +1,11 @@
using CertMgr.Core.Utils;
namespace CertMgr.Core.Cli;
public class CliException : Exception
{
public CliException(string messageFormat, params object[] messageArgs)
: base(StringFormatter.Format(messageFormat, messageArgs))
{
}
}

View File

@@ -0,0 +1,61 @@
using CertMgr.Core.Log;
namespace CertMgr.Core.Cli;
public sealed class CliParser
{
public Task<RawArguments> ParseAsync(string[] args, CancellationToken cancellationToken)
{
CLog.Info("parsing arguments...");
RawArguments rawArgs = new RawArguments(args.Length);
foreach (string arg in args)
{
int nameStartIndex = -1;
if (arg.StartsWith("--"))
{
nameStartIndex = 2;
}
else if (arg.StartsWith("-"))
{
nameStartIndex = 1;
}
if (nameStartIndex < 0)
{
continue;
}
int separatorIndex = arg.IndexOf('=', nameStartIndex);
if (separatorIndex < 0)
{
// --my-value
string name = arg.Substring(nameStartIndex);
rawArgs.Add(new RawArgument(name));
}
else if (separatorIndex == nameStartIndex + 1)
{
// --= or --=xxx => name missing
}
else
{
// --myvalue= or --my-value=xxx
string name = arg.Substring(nameStartIndex, separatorIndex - nameStartIndex);
if (separatorIndex < arg.Length)
{
string value = arg.Substring(separatorIndex + 1);
rawArgs.Add(new RawArgument(name, value));
}
else
{
rawArgs.Add(new RawArgument(name));
}
}
}
CLog.Info("parsing arguments... done (found {0} arguments)", rawArgs.Count);
return Task.FromResult(rawArgs);
}
}

View File

@@ -0,0 +1,58 @@
using System.Diagnostics;
namespace CertMgr.Core.Cli;
public sealed class RawArgument
{
internal RawArgument(string name)
{
Name = name;
Values = new List<string>(0);
}
internal RawArgument(string name, string value)
{
Name = name;
Values = new List<string> { value };
}
internal RawArgument(string name, IReadOnlyList<string> values, string value)
{
Name = name;
List<string> tmp = new List<string>(values.Count + 1);
Values = tmp;
tmp.AddRange(values);
tmp.Add(value);
}
internal RawArgument(string name, IReadOnlyList<string> values1, IReadOnlyList<string> values2)
{
Name = name;
List<string> tmp = new List<string>(values1.Count + values2.Count);
Values = tmp;
tmp.AddRange(values1);
tmp.AddRange(values2);
}
public string Name { [DebuggerStepThrough] get; }
public IReadOnlyList<string> Values { [DebuggerStepThrough] get; }
public override string ToString()
{
string result;
switch (Values.Count)
{
case 0:
result = string.Format("name = '{0}', no-values", Name);
break;
case 1:
result = string.Format("name = '{0}', value = '{1}'", Name, Values.First() ?? "<null>");
break;
default:
result = string.Format("name = '{0}', first-value = '{1}', values-count = '{2}'", Name, Values[0] ?? "<null>", Values.Count);
break;
}
return result;
}
}

View File

@@ -0,0 +1,61 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
namespace CertMgr.Core.Cli;
public sealed class RawArguments : IReadOnlyCollection<RawArgument>
{
private Dictionary<string, RawArgument> _items;
internal RawArguments()
{
_items = new Dictionary<string, RawArgument>();
}
internal RawArguments(int initialCapacity)
{
_items = new Dictionary<string, RawArgument>(initialCapacity);
}
public int Count => _items.Count;
public IReadOnlyList<string> Names => _items.Keys.ToList();
public RawArgument this[string name]
{
get => _items[name];
set => _items[name] = value;
}
internal void Add(RawArgument rawArg)
{
if (_items.Remove(rawArg.Name, out RawArgument? existing))
{
_items.Add(rawArg.Name, new RawArgument(rawArg.Name, existing.Values, rawArg.Values));
}
else
{
_items.Add(rawArg.Name, rawArg);
}
}
public bool TryGet(string name, [NotNullWhen(true)] out RawArgument? rawArg)
{
return _items.TryGetValue(name, out rawArg);
}
public IEnumerator<RawArgument> GetEnumerator()
{
return _items.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public override string ToString()
{
return string.Format("count = {0}", _items.Count);
}
}

View File

@@ -0,0 +1,6 @@
namespace CertMgr.Core.Converters;
public class ConverterContext
{
public static readonly ConverterContext Empty = new ConverterContext();
}

View File

@@ -0,0 +1,15 @@
using CertMgr.Core.Utils;
namespace CertMgr.Core.Converters;
public sealed class ConverterException : Exception
{
internal ConverterException(string messageFormat, params object?[] messageArgs)
: base(StringFormatter.Format(messageFormat, messageArgs))
{
}
internal ConverterException(Exception? innerException, string messageFormat, params object?[] messageArgs)
: base(StringFormatter.Format(messageFormat, messageArgs), innerException)
{
}
}

View File

@@ -0,0 +1,19 @@
using CertMgr.Core.Converters.Impl;
namespace CertMgr.Core.Converters;
public sealed class ConverterFactory
{
internal ConverterFactory()
{
}
public ConverterStash CreateDefault()
{
ConverterStash stash = new ConverterStash();
stash.Set(typeof(int), typeof(IntConverter));
stash.Set(typeof(string), typeof(StringConverter));
stash.Set(typeof(Enum), typeof(EnumConverter));
return stash;
}
}

View File

@@ -0,0 +1,102 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace CertMgr.Core.Converters;
public sealed class ConverterStash
{
private readonly Dictionary<Type, ConverterDescriptor> _items;
internal ConverterStash()
{
_items = new Dictionary<Type, ConverterDescriptor>();
}
public bool Set(Type conversionType, Type converterType, bool replace = false)
{
bool added = false;
if (_items.ContainsKey(conversionType))
{
if (replace)
{
_items[conversionType] = new ConverterDescriptor(converterType);
added = true;
}
}
else
{
_items.Add(conversionType, new ConverterDescriptor(converterType));
added = true;
}
return added;
}
public bool TryGet(Type type, [NotNullWhen(true)] out IValueConverter? converter)
{
converter = null;
if (_items.TryGetValue(type, out ConverterDescriptor? descriptor))
{
converter = descriptor.Instance;
}
else if (type.IsEnum)
{
if (_items.TryGetValue(typeof(Enum), out descriptor))
{
converter = descriptor.Instance;
}
}
return converter != null;
}
public override string ToString()
{
return string.Format("count = {0}", _items.Count);
}
private sealed class ConverterDescriptor
{
private IValueConverter? _converter;
public ConverterDescriptor(Type converterType)
{
ConverterType = converterType;
}
public Type ConverterType { [DebuggerStepThrough] get; }
public IValueConverter Instance
{
get
{
if (_converter == null)
{
lock (this)
{
if (_converter == null)
{
try
{
_converter = (IValueConverter?)Activator.CreateInstance(ConverterType);
}
catch (Exception e)
{
throw new ConverterException(e, "Failed to create instance of converter of type '{0}'", ConverterType.Name);
}
}
}
}
if (_converter == null)
{
throw new ConverterException("Failed to create instance of converter of type '{0}'", ConverterType.Name);
}
return _converter;
}
}
}
}

View File

@@ -0,0 +1,13 @@
using System.Diagnostics;
namespace CertMgr.Core.Converters;
public class EnumConverterContext : ConverterContext
{
internal EnumConverterContext(Type targetType)
{
TargetType = targetType;
}
public Type TargetType { [DebuggerStepThrough] get; }
}

View File

@@ -0,0 +1,6 @@
namespace CertMgr.Core.Converters;
public interface IValueConverter
{
Task<object?> ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,6 @@
namespace CertMgr.Core.Converters;
public interface IValueConverter<T> : IValueConverter
{
new Task<T?> ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,43 @@
using CertMgr.Core.Utils;
namespace CertMgr.Core.Converters.Impl;
public sealed class EnumConverter : ValueConverter<Enum>
{
protected override Task<Enum?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
Type resultType;
bool isNullable = targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>);
if (isNullable)
{
Type[] args = targetType.GetGenericArguments();
if (args.Length != 1)
{
throw new ConverterException("Cannot convert nullable type '{0}' to enum", targetType.ToString(false));
}
if (args[0].IsEnum)
{
resultType = args[0];
}
else
{
throw new ConverterException("Cannot convert nullable type '{0}' as enum as it is not enum", targetType.ToString(false));
}
}
else
{
if (!targetType.IsEnum)
{
resultType = targetType;
}
else
{
throw new ConverterException("Cannot convert type '{0}' as enum as it is not enum", targetType.ToString(false));
}
}
object? result = Enum.Parse(resultType, rawValue, true);
return Task.FromResult((Enum?)result);
}
}

View File

@@ -0,0 +1,15 @@
namespace CertMgr.Core.Converters.Impl;
public sealed class EnumNullableConverter : ValueConverter<Enum?>
{
protected override Task<Enum?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
if (!targetType.IsEnum)
{
throw new ConverterException("Cannot convert type '{0}' as enum as it is not enum", targetType.Name);
}
object? result = Enum.Parse(targetType, rawValue, true);
return Task.FromResult((Enum?)result);
}
}

View File

@@ -0,0 +1,10 @@
namespace CertMgr.Core.Converters.Impl;
public sealed class IntConverter : ValueConverter<int>
{
protected override Task<int> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
int result = int.Parse(rawValue);
return Task.FromResult(result);
}
}

View File

@@ -0,0 +1,121 @@
using System.Diagnostics.CodeAnalysis;
using CertMgr.Core.Log;
using CertMgr.Core.Storage;
namespace CertMgr.Core.Converters.Impl;
internal sealed class StorageConverter : ValueConverter<IStorage>
{
private const char Separator = '|';
protected override Task<IStorage?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
ReadOnlySpan<char> rawSpan = 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":
if (!TryGetFileStore(storageDefinition, out storage))
{
storage = EmptyStorage.Empty;
}
break;
default:
storage = EmptyStorage.Empty;
break;
}
return Task.FromResult((IStorage?)storage);
}
private bool TryGetFileStore(ReadOnlySpan<char> storageDefinition, [NotNullWhen(true)] out IStorage? storage)
{
// 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
storage = 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.Concat("Unsupported store-mode '", firstPart, "'"));
storeMode = defaultStoreMode;
}
}
else
{
// no split-char => just filename
storeMode = defaultStoreMode;
filename = storageDefinition;
}
storage = new FileStorage(filename.ToString(), storeMode);
return true;
}
}

View File

@@ -0,0 +1,10 @@
namespace CertMgr.Core.Converters.Impl;
public sealed class StringConverter : ValueConverter<string>
{
protected override Task<string?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
return Task.FromResult(rawValue);
}
}

View File

@@ -0,0 +1,49 @@
namespace CertMgr.Core.Converters.Impl;
public sealed class TimeSpanConverter : ValueConverter<TimeSpan?>
{
protected override Task<TimeSpan?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(rawValue) || rawValue.Length == 1)
{
return Task.FromResult((TimeSpan?)null);
}
ReadOnlySpan<char> rawSpan = rawValue.AsSpan();
ReadOnlySpan<char> valueSpan = rawSpan.Slice(0, rawSpan.Length - 1);
char unit = char.ToLower(rawValue[rawValue.Length - 1]);
TimeSpan? result;
if (int.TryParse(valueSpan, out int value))
{
switch (unit)
{
case 's':
result = TimeSpan.FromSeconds(value);
break;
case 'm':
result = TimeSpan.FromMinutes(value);
break;
case 'h':
result = TimeSpan.FromHours(value);
break;
case 'd':
result = TimeSpan.FromDays(value);
break;
case 'y':
result = TimeSpan.FromDays(value * 365);
break;
default:
result = null;
break;
}
}
else
{
result = null;
}
return Task.FromResult(result);
}
}

View File

@@ -0,0 +1,11 @@
namespace CertMgr.Core.Converters;
public abstract class ValueConverter : IValueConverter
{
public Task<object?> ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
return DoConvertAsync(rawValue, targetType, cancellationToken);
}
protected abstract Task<object?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,30 @@
namespace CertMgr.Core.Converters;
public abstract class ValueConverter<T> : IValueConverter<T>
{
public Task<T?> ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
if (typeof(T) != targetType)
{
throw new ConverterException("This converter ('{0}') converts string value to type '{1}' but type '{2}' is expected this time", GetType().Name, typeof(T), targetType.Name);
}
try
{
return DoConvertAsync(rawValue, targetType, cancellationToken);
}
catch (Exception e)
{
throw new ConverterException(e, "Failed to convert value '{0}' to type '{1}'", rawValue ?? "<null>", typeof(T).Name);
}
}
async Task<object?> IValueConverter.ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
{
T? typedValue = await DoConvertAsync(rawValue, targetType, cancellationToken);
return typedValue;
}
protected abstract Task<T?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,9 @@
namespace CertMgr.Core.Exceptions;
public class UnsupportedValueException : Exception
{
public UnsupportedValueException(object unsupportedValue)
: base(string.Format("Unsupported value '{0}' of type '{1}'", unsupportedValue != null ? unsupportedValue.ToString() : "<null>", unsupportedValue != null ? unsupportedValue.GetType().FullName : "<null>"))
{
}
}

View File

@@ -0,0 +1,24 @@
using System.Diagnostics;
namespace CertMgr.Core;
internal sealed class JobDescriptor
{
internal JobDescriptor(Type jobType, Type settingsType, Type? resultType)
{
JobType = jobType;
SettingsType = settingsType;
ResultType = resultType;
}
public Type JobType { [DebuggerStepThrough] get; }
public Type SettingsType { [DebuggerStepThrough] get; }
public Type? ResultType { [DebuggerStepThrough] get; }
public override string ToString()
{
return string.Format("job-type = '{0}', settings-type = '{1}', result-type = '{2}'", JobType.Name, SettingsType.Name, ResultType?.Name ?? "<null>");
}
}

125
certmgr/Core/JobExecutor.cs Normal file
View File

@@ -0,0 +1,125 @@
using System.Reflection;
using System.Text;
using CertMgr.Core.Cli;
using CertMgr.Core.Jobs;
using CertMgr.Core.Log;
using CertMgr.Core.Utils;
using CertMgr.Core.Validation;
namespace CertMgr.Core;
internal sealed class JobExecutor
{
public async Task<int> ExecuteAsync(string[] args, CancellationToken cancellationToken)
{
int errorLevel = JobResult.Failure;
CliParser parser = new CliParser();
RawArguments rawArgs = await parser.ParseAsync(args, cancellationToken).ConfigureAwait(false);
if (rawArgs.TryGet("job", out RawArgument? rawArg))
{
JobRegistry jobs = new JobRegistry();
await jobs.LoadAsync(cancellationToken).ConfigureAwait(false);
if (jobs.TryGet(rawArg.Values.First(), out JobDescriptor? descriptor))
{
IJob job = await CreateJobAsync(descriptor, rawArgs, cancellationToken).ConfigureAwait(false);
JobResult result = await job.ExecuteAsync(cancellationToken).ConfigureAwait(false);
errorLevel = result.ErrorLevel;
}
else
{
CLog.Error("Unknown job '{0}'", rawArg.Values.First());
errorLevel = JobResult.Failure;
}
}
else
{
CLog.Error("Argument '{0}' must be specified", "job");
errorLevel = JobResult.Failure;
}
return errorLevel;
}
private async Task<IJob> CreateJobAsync(JobDescriptor descriptor, RawArguments rawArgs, CancellationToken cancellationToken)
{
IJob? job;
try
{
job = (IJob?)Activator.CreateInstance(descriptor.JobType);
}
catch (Exception e)
{
throw new CertMgrException(e, "Failed to instantiate job '{0}'", descriptor.JobType.Name);
}
if (job == null)
{
throw new CertMgrException("Failed to instantiate job '{0}' (Possibly missing parameterless ctor?)", descriptor.JobType.Name);
}
SettingsBuilder settingsBuilder = new SettingsBuilder(rawArgs, descriptor.SettingsType);
JobSettings settings = await settingsBuilder.LoadAsync(cancellationToken).ConfigureAwait(false);
if (!settings.ValidationResults.IsValid)
{
StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped();
StringBuilder sb = lease.Builder;
sb.AppendFormat("Validation of settings for job '{0}' found {1} error(s):", job.Name, settings.ValidationResults.Count(vr => !vr.IsValid));
sb.AppendLine();
foreach (ValidationResult vr in settings.ValidationResults)
{
if (vr.IsValid)
{
continue;
}
sb.AppendFormat("\t- {0}: {1}", vr.PropertyName, vr.Justification);
sb.AppendLine();
}
throw new CertMgrException(sb.ToString());
}
MethodInfo? settingsSetter = GetPropertyWithPrivateSetter(descriptor.JobType, "Settings");
if (settingsSetter != null)
{
settingsSetter.Invoke(job, [settings]);
}
else
{
throw new CertMgrException("Failed to initialize job '{0}'. Missing property 'Settings' (Possibly the job doesn't inherit from '{1}'?)", descriptor.JobType.Name, typeof(Job<>).Name);
}
return job;
}
private static MethodInfo? GetPropertyWithPrivateSetter(Type type, string name)
{
MethodInfo? setter = null;
for (Type currentType = type; currentType != null; currentType = currentType.BaseType)
{
if (currentType.ContainsGenericParameters)
{
continue;
}
PropertyInfo? property = currentType.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
if (property == null)
{
continue;
}
setter = property.GetSetMethod(true);
if (setter != null)
{
break;
}
}
return setter;
}
}

153
certmgr/Core/JobRegistry.cs Normal file
View File

@@ -0,0 +1,153 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text;
using CertMgr.Core.Jobs;
using CertMgr.Core.Log;
using CertMgr.Core.Utils;
namespace CertMgr.Core;
internal sealed class JobRegistry
{
private readonly Dictionary<string, JobDescriptor> _items;
public JobRegistry()
{
_items = new Dictionary<string, JobDescriptor>();
}
public IReadOnlyList<string> Names => _items.Keys.ToList();
public Task LoadAsync(CancellationToken cancellationToken)
{
CLog.Info("Loading job registry...");
if (TryGetAssemblyTypes(Assembly.GetExecutingAssembly(), out Type[] types))
{
foreach (Type type in types)
{
if (type.IsAbstract)
{
continue;
}
JobUtils.TryGetJobName(type, out string? jobName, out string? errorMessage);
TryGetGenericTypes(type, out Type? settingsType, out Type? resultType);
if (jobName != null && settingsType != null)
{
if (_items.TryGetValue(jobName, out JobDescriptor? alreadyRegisteredType))
{
CLog.Error("Job '{0}' has multiple implementations. Types '{0}' and '{1}'. First win.", jobName, alreadyRegisteredType.JobType.ToString(false), type.ToString(false));
}
else
{
_items.Add(jobName, new JobDescriptor(type, settingsType, resultType));
}
}
else
{
if (jobName == null && settingsType != null)
{
CLog.Error(errorMessage);
}
if (jobName != null && settingsType == null)
{
CLog.Error("Job '{0}' has job-name '{1}' but the class doesn't inherit from '{2}'", type.ToString(false), jobName, typeof(Job<>).ToString(false));
}
}
}
}
CLog.Info("Loading job registry... done (registered {0} jobs)", _items.Count);
return Task.CompletedTask;
}
public bool TryGet(string name, [NotNullWhen(true)] out JobDescriptor? jobDescriptor)
{
jobDescriptor = null;
if (_items.TryGetValue(name, out JobDescriptor? descriptor))
{
jobDescriptor = descriptor;
}
return jobDescriptor != null;
}
private bool TryGetGenericTypes(Type type, [NotNullWhen(true)] out Type? settingType, out Type? resultType)
{
settingType = null;
resultType = null;
for (Type t = type; t != null && type != typeof(object); t = t.BaseType!)
{
if (t.IsGenericType)
{
Type[] genericTypes = t.GetGenericArguments();
Type currentGenericType = t.GetGenericTypeDefinition();
if (currentGenericType == typeof(Job<,>))
{
if (typeof(JobSettings).IsAssignableFrom(genericTypes[0]))
{
settingType = genericTypes[0];
resultType = genericTypes[1];
}
else
{
settingType = genericTypes[1];
resultType = genericTypes[0];
}
}
else if (currentGenericType == typeof(JobBase<>))
{
if (typeof(JobSettings).IsAssignableFrom(genericTypes[0]))
{
settingType = genericTypes[0];
break;
}
}
}
}
return settingType != null;
}
private bool TryGetAssemblyTypes(Assembly assembly, out Type[] types)
{
bool succeeded;
try
{
types = assembly.GetTypes();
succeeded = true;
}
catch (ReflectionTypeLoadException e)
{
using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(256);
StringBuilder sb = lease.Builder;
sb.AppendFormat("Failed to load assembly types. {0}: {1} (exceptions-count = {2}).{3}", e.GetType().ToString(false), e.Message, e.LoaderExceptions?.Length ?? -1, Environment.NewLine);
foreach (Exception? le in e.LoaderExceptions ?? Array.Empty<Exception>())
{
if (le == null)
{
continue;
}
sb.AppendFormat("\t{0}: {1}{2}", le.GetType().ToString(false), le.Message, Environment.NewLine);
}
CLog.Error(StringBuilderCache.GetStringAndRelease(sb));
types = Array.Empty<Type>();
succeeded = false;
}
return succeeded;
}
public override string ToString()
{
return string.Format("count = {0}", _items.Count);
}
}

10
certmgr/Core/Jobs/IJob.cs Normal file
View File

@@ -0,0 +1,10 @@
using System.Diagnostics;
namespace CertMgr.Core.Jobs;
public interface IJob
{
string Name { [DebuggerStepThrough] get; }
Task<JobResult> ExecuteAsync(CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,6 @@
namespace CertMgr.Core.Jobs;
public interface IJob<TResult> : IJob
{
new Task<JobResult<TResult>> ExecuteAsync(CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,20 @@
using System.Diagnostics;
namespace CertMgr.Core.Jobs;
public abstract class JobBase<TSettings> where TSettings : JobSettings, new()
{
protected JobBase()
{
Name = JobUtils.GetJobName(GetType());
}
public string Name { [DebuggerStepThrough] get; }
public TSettings Settings { [DebuggerStepThrough] get; [DebuggerStepThrough] private set; }
public override string ToString()
{
return string.Format("name = '{0}', settings-type = '{1}'", Name, Settings?.GetType().Name ?? "<null>");
}
}

View File

@@ -0,0 +1,15 @@
using CertMgr.Core.Utils;
namespace CertMgr.Core.Jobs;
public class JobException : Exception
{
internal JobException(string messageFormat, params object?[] messageArgs)
: base(StringFormatter.Format(messageFormat, messageArgs))
{
}
internal JobException(Exception? innerException, string messageFormat, params object?[] messageArgs)
: base(StringFormatter.Format(messageFormat, messageArgs), innerException)
{
}
}

View File

@@ -0,0 +1,41 @@

namespace CertMgr.Core.Jobs;
public abstract class Job<TResult, TSettings> : JobBase<TSettings>, IJob<TResult> where TSettings : JobSettings, new()
{
public async Task<JobResult<TResult>> ExecuteAsync(CancellationToken cancellationToken)
{
try
{
JobResult<TResult> result = await DoExecuteAsync(cancellationToken).ConfigureAwait(false);
return result;
}
catch (Exception e)
{
return CreateFailure(default, e, "Failed to execute job '{0}'", Name);
}
}
protected abstract Task<JobResult<TResult>> DoExecuteAsync(CancellationToken cancellationToken);
async Task<JobResult> IJob.ExecuteAsync(CancellationToken cancellationToken)
{
JobResult<TResult> result = await ExecuteAsync(cancellationToken);
return result;
}
protected JobResult<TResult> CreateSuccess(TResult? result, string messageFormat, params object[] messageArgs)
{
return new JobResult<TResult>(this, result, JobResult.Success, messageFormat, messageArgs);
}
protected JobResult<TResult> CreateFailure(TResult? result, string messageFormat, params object[] messageArgs)
{
return new JobResult<TResult>(this, result, JobResult.Success, messageFormat, messageArgs);
}
protected JobResult<TResult> CreateFailure(TResult? result, Exception exception, string messageFormat, params object[] messageArgs)
{
return new JobResult<TResult>(this, result, JobResult.Success, messageFormat, messageArgs);
}
}

View File

@@ -0,0 +1,41 @@
using System.Diagnostics;
using CertMgr.Core.Utils;
namespace CertMgr.Core.Jobs;
public class JobResult
{
public const int Success = 0;
public const int Failure = 1;
internal JobResult(IJob job, int errorLevel, string messageFormat, params object[] messageArgs)
{
Job = job;
ErrorLevel = errorLevel;
Message = StringFormatter.Format(messageFormat, messageArgs);
}
internal JobResult(IJob job, int errorLevel, Exception? exception, string messageFormat, params object[] messageArgs)
{
Job = job;
ErrorLevel = errorLevel;
Message = StringFormatter.Format(messageFormat, messageArgs);
Exception = exception;
}
public IJob Job { [DebuggerStepThrough] get; }
public int ErrorLevel { [DebuggerStepThrough] get; }
public string Message { [DebuggerStepThrough] get; }
public Exception? Exception { [DebuggerStepThrough] get; }
public bool IsSuccess => ErrorLevel == 0;
public override string ToString()
{
return string.Format("result = {0}, message = '{1}'", IsSuccess ? "success" : "failure", Message);
}
}

View File

@@ -0,0 +1,20 @@
using System.Diagnostics;
namespace CertMgr.Core.Jobs;
public class JobResult<TResult> : JobResult
{
internal JobResult(IJob<TResult> job, TResult? result, int errorLevel, string messageFormat, params object[] messageArgs)
: base(job, errorLevel, messageFormat, messageArgs)
{
Result = result;
}
internal JobResult(IJob<TResult> job, TResult? result, int errorLevel, Exception? exception, string messageFormat, params object[] messageArgs)
: base(job, errorLevel, exception, messageFormat, messageArgs)
{
Result = result;
}
public TResult? Result { [DebuggerStepThrough] get; }
}

36
certmgr/Core/Jobs/JobS.cs Normal file
View File

@@ -0,0 +1,36 @@
namespace CertMgr.Core.Jobs;
public abstract class Job<TSettings> : JobBase<TSettings>, IJob where TSettings : JobSettings, new()
{
public async Task<JobResult> ExecuteAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
JobResult result = await DoExecuteAsync(cancellationToken).ConfigureAwait(false);
return result;
}
catch (Exception e)
{
return CreateFailure(e, "Failed to execute job '{0}'", Name);
}
}
protected abstract Task<JobResult> DoExecuteAsync(CancellationToken cancellationToken);
protected JobResult CreateSuccess(string messageFormat, params object[] messageArgs)
{
return new JobResult(this, JobResult.Success, messageFormat, messageArgs);
}
protected JobResult CreateFailure(string messageFormat, params object[] messageArgs)
{
return new JobResult(this, JobResult.Failure, messageFormat, messageArgs);
}
protected JobResult CreateFailure(Exception exception, string messageFormat, params object[] messageArgs)
{
return new JobResult(this, JobResult.Failure, exception, messageFormat, messageArgs);
}
}

View File

@@ -0,0 +1,25 @@
using System.Diagnostics;
using CertMgr.Core.Validation;
namespace CertMgr.Core.Jobs;
public abstract class JobSettings
{
protected JobSettings()
{
ValidationResults = new ValidationResults();
}
public ValidationResults ValidationResults { [DebuggerStepThrough] get; }
public async Task ValidateAsync(CancellationToken cancellationToken)
{
await DoValidateAsync(ValidationResults, cancellationToken);
}
protected virtual Task DoValidateAsync(ValidationResults results, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,54 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using CertMgr.Core.Cli;
namespace CertMgr.Core.Jobs;
internal static class JobUtils
{
private const string IdFieldName = "ID";
internal static string GetJobName(Type jobType)
{
if (!TryGetJobName(jobType, out string? name, out string? errorMessage))
{
throw new CliException(errorMessage);
}
return name;
}
internal static bool TryGetJobName(Type jobType, [NotNullWhen(true)] out string? name, [NotNullWhen(false)] out string? errorMessage)
{
name = null;
errorMessage = null;
FieldInfo? finfo = jobType.GetField(IdFieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly);
if (finfo != null)
{
if (finfo.FieldType == typeof(string))
{
string? value = finfo.GetValue(null) as string;
if (!string.IsNullOrEmpty(value))
{
name = value;
}
else
{
errorMessage = string.Format("Value of field '{0}' in class '{1}' must not be null or empty (actual = '{2}')", IdFieldName, jobType.Name, value == null ? "<null>" : "<empty>");
}
}
else
{
errorMessage = string.Format("Type of field '{0}' in class '{1}' must be '{2}' (actual = '{3})", IdFieldName, jobType.Name, typeof(string), finfo.FieldType.Name);
}
}
else
{
errorMessage = string.Format("Class '{0}' doesn't have field '{1}' of type '{2}'", jobType.Name, IdFieldName, typeof(string));
}
return name != null;
}
}

117
certmgr/Core/Log/CLog.cs Normal file
View File

@@ -0,0 +1,117 @@
using System.Diagnostics;
using System.Text;
using CertMgr.Core.Utils;
namespace CertMgr.Core.Log;
public static class CLog
{
public const string TimestampFormat = "HH:mm:ss,fff";
private const string ConsoleHeaderFormat = "({0})({1})(@{2})(#{3}) ";
private static readonly object _syncObj = new object();
public static void Debug(string messageFormat, params object?[] messageArgs)
{
Write(LogLevel.Debug, null, messageFormat, messageArgs);
}
public static void Info(string messageFormat, params object?[] messageArgs)
{
Write(LogLevel.Info, null, messageFormat, messageArgs);
}
public static void Warning(string messageFormat, params object?[] messageArgs)
{
Write(LogLevel.Warning, null, messageFormat, messageArgs);
}
public static void Error(string messageFormat, params object?[] messageArgs)
{
Write(LogLevel.Error, null, messageFormat, messageArgs);
}
public static void Error(Exception? exception, string messageFormat, params object?[] messageArgs)
{
Write(LogLevel.Error, exception, messageFormat, messageArgs);
}
public static void Write(LogLevel level, string messageFormat, params object?[] messageArgs)
{
Write(level, null, messageFormat, messageArgs);
}
public static void Write(LogLevel level, Exception? exception, string messageFormat, params object?[] messageArgs)
{
using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(exception != null ? 256 : 128);
StringBuilder sb = lease.Builder;
AppendHeader(sb, ConsoleHeaderFormat, level, DateTime.Now, Process.GetCurrentProcess().Id, Thread.CurrentThread.ManagedThreadId);
AppendMessage(sb, exception, messageFormat, messageArgs);
WriteColorized(level, sb.ToString());
}
public static void WriteNoHeader(LogLevel level, string message)
{
WriteColorized(level, message);
}
private static void AppendHeader(StringBuilder sb, string headerFormat, LogLevel level, DateTime timestamp, int processId, int threadId)
{
char levelChar = level.ToString()[0];
sb.AppendFormat(headerFormat, levelChar, timestamp.ToString(TimestampFormat), processId.ToString().PadLeft(5), threadId.ToString().PadLeft(3));
}
private static void AppendMessage(StringBuilder sb, Exception? exception, string messageFormat, params object?[] messageArgs)
{
sb.AppendFormat(messageFormat, messageArgs);
if (exception != null)
{
sb.AppendLine();
sb.AppendLine(exception.ToString());
}
}
internal static void WriteColorized(LogLevel level, string message)
{
ConsoleColor foreColor = GetForeColor(level);
lock (_syncObj)
{
Console.ForegroundColor = foreColor;
Console.WriteLine(message);
Console.ResetColor();
}
}
private static ConsoleColor GetForeColor(LogLevel level)
{
ConsoleColor color;
switch (level)
{
case LogLevel.Debug:
color = ConsoleColor.Gray;
break;
case LogLevel.Info:
color = ConsoleColor.White;
break;
case LogLevel.Warning:
color = ConsoleColor.DarkYellow;
break;
case LogLevel.Error:
color = ConsoleColor.DarkRed;
break;
case LogLevel.Critical:
color = ConsoleColor.DarkMagenta;
break;
default:
color = ConsoleColor.DarkCyan;
break;
}
return color;
}
}

View File

@@ -0,0 +1,10 @@
namespace CertMgr.Core.Log;
public enum LogLevel
{
Debug = 1,
Info,
Warning,
Error,
Critical
}

View File

@@ -0,0 +1,332 @@
using System.Collections;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using CertMgr.Core.Attributes;
using CertMgr.Core.Cli;
using CertMgr.Core.Converters;
using CertMgr.Core.Jobs;
using CertMgr.Core.Log;
using CertMgr.Core.Validation;
namespace CertMgr.Core;
internal sealed class SettingsBuilder
{
private readonly RawArguments _rawArgs;
private readonly Type _settingsType;
internal SettingsBuilder(RawArguments rawArgs, Type settingsType)
{
_rawArgs = rawArgs;
_settingsType = settingsType;
}
public async Task<JobSettings> LoadAsync(CancellationToken cancellationToken)
{
JobSettings settings = CreateSettingsInstance();
foreach ((PropertyInfo propertyInfo, SettingAttribute settingAttribute) in GetPropertiesWithSettingAttribute())
{
if (TryGetRawArgument(settingAttribute, out RawArgument? rawArg))
{
(bool isCollection, Type elementType) = GetValueType(propertyInfo);
(bool converted, object? convertedValue) = await ConvertRawValueAsync(settingAttribute, rawArg, isCollection, elementType, cancellationToken).ConfigureAwait(false);
if (converted)
{
propertyInfo.SetValue(settings, convertedValue);
if (settingAttribute.Validator != null)
{
ISettingValidator? validator = (ISettingValidator?)Activator.CreateInstance(settingAttribute.Validator, [settingAttribute.Name]);
if (validator != null)
{
ValidationResult valres = await validator.ValidateAsync(convertedValue, cancellationToken).ConfigureAwait(false);
settings.ValidationResults.Add(valres);
}
}
}
}
else if (settingAttribute.IsMandatory)
{
ValidationResult valres = new ValidationResult(settingAttribute.Name, false, "Mandatory argument is missing");
settings.ValidationResults.Add(valres);
CLog.Error("mandatory argument '{0}' is missing", settingAttribute.Name);
}
else if (settingAttribute.Default != null)
{
(bool isCollection, Type elementType) = GetValueType(propertyInfo);
if (settingAttribute.Default.GetType() == elementType)
{
propertyInfo.SetValue(settings, settingAttribute.Default);
}
else
{
CLog.Error("Default value for argument '{0}' is specified, but its type is '{1}' instead of expected '{2}'", settingAttribute.Name, settingAttribute.Default?.GetType().Name ?? "<null>", elementType);
}
}
}
await settings.ValidateAsync(cancellationToken).ConfigureAwait(false);
return settings;
}
private bool TryGetRawArgument(SettingAttribute settingAttribute, [NotNullWhen(true)] out RawArgument? rawArg)
{
rawArg = null;
if (!_rawArgs.TryGet(settingAttribute.Name, out rawArg))
{
if (settingAttribute.AlternateNames != null)
{
foreach (string altName in settingAttribute.AlternateNames)
{
if (_rawArgs.TryGet(altName, out rawArg))
{
break;
}
}
}
}
return rawArg != null;
}
private (bool isCollection, Type elementType) GetValueType(PropertyInfo propertyInfo)
{
(bool isCollection, Type? elemType) = UnwrapCollection(propertyInfo.PropertyType);
Type targetType = isCollection ? elemType! : propertyInfo.PropertyType;
return (isCollection, targetType);
}
private IEnumerable<(PropertyInfo, SettingAttribute)> GetPropertiesWithSettingAttribute()
{
foreach (PropertyInfo propertyInfo in _settingsType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
SettingAttribute? settingAttribute = propertyInfo.GetCustomAttribute<SettingAttribute>();
if (settingAttribute is null)
{
continue;
}
yield return (propertyInfo, settingAttribute);
}
}
private JobSettings CreateSettingsInstance()
{
object? instance;
try
{
instance = Activator.CreateInstance(_settingsType);
if (instance == null)
{
throw new CertMgrException("Failed to create instance for settings of type '{0}'", _settingsType.Name);
}
}
catch (Exception e)
{
throw new CertMgrException(e, "Failed to create instance for settings of type '{0}'", _settingsType.Name);
}
if (instance is not JobSettings settings)
{
throw new CertMgrException("Failed to create instance for settings of type '{0}'. The type is not of type '{1}'", _settingsType.Name, typeof(JobSettings).Name);
}
return settings;
}
private async Task<(bool success, object? convertedValue)> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type targetType, CancellationToken cancellationToken)
{
bool success = false;
object? convertedValue = null;
if (isCollection)
{
if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter))
{
List<object?> values = new List<object?>();
foreach (string rawValue in rawArg.Values)
{
convertedValue = await customConverter.ConvertAsync(rawValue, targetType, cancellationToken).ConfigureAwait(false);
values.Add(convertedValue);
}
convertedValue = values;
success = true;
}
else
{
List<object?> values = new List<object?>();
foreach (string rawValue in rawArg.Values)
{
if (TryConvertValue(rawValue, targetType, out convertedValue))
{
values.Add(convertedValue);
}
}
convertedValue = values;
success = true;
}
}
else
{
if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter))
{
convertedValue = await customConverter.ConvertAsync(rawArg.Values.First(), targetType, cancellationToken).ConfigureAwait(false);
success = true;
}
else if (TryConvertValue(rawArg.Values.First(), targetType, out convertedValue))
{
success = true;
}
else
{
}
}
return (success, convertedValue);
}
private bool TryGetCustomConverter(SettingAttribute settingAttribute, [NotNullWhen(true)] out IValueConverter? customConverter)
{
customConverter = null;
Type? valueConverter = settingAttribute.Converter;
if (valueConverter != null)
{
if (typeof(IValueConverter).IsAssignableFrom(valueConverter))
{
customConverter = (IValueConverter?)Activator.CreateInstance(valueConverter);
}
else
{
CLog.Error("Argument '{0}' has converter specified but its type doesn't implement '{1}' and cannot be used", settingAttribute.Name, typeof(IValueConverter));
}
}
return customConverter != null;
}
private bool TryConvertValue(string rawValue, Type targetType, out object? convertedValue)
{
convertedValue = null;
if (targetType.IsEnum)
{
if (Enum.TryParse(targetType, rawValue, true, out object? result))
{
convertedValue = result;
}
}
else
{
TypeConverter converter = TypeDescriptor.GetConverter(targetType);
if (converter.CanConvertFrom(typeof(string)))
{
convertedValue = converter.ConvertFrom(null, CultureInfo.InvariantCulture, rawValue)!;
}
else if (targetType == typeof(string))
{
convertedValue = rawValue;
}
else
{
convertedValue = null;
}
}
return convertedValue != null;
}
private static (bool isCollection, Type? elementType) UnwrapCollection(Type type)
{
if (type == typeof(string))
{
return (false, null);
}
Type? underlying = Nullable.GetUnderlyingType(type);
if (underlying != null)
{
type = underlying;
}
if (type.IsArray)
{
return (true, type.GetElementType()!);
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return (true, type.GetGenericArguments()[0]);
}
if (type.GetInterfaces().Any(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
return (true, type.GetGenericArguments()[0]);
}
// if (type.IsGenericType && typeof(IEnumerable<>).IsAssignableFrom(type.GetGenericTypeDefinition()))
// {
// return (true, type.GetGenericArguments()[0]);
// }
// if (t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>)))
// {
// return (true, t.GetInterfaces().First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>)).GetGenericArguments()[0]);
// }
// if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(List<>)))
// {
// return (true, t.GetGenericArguments()[0]);
// }
return (false, null);
}
private static bool IsEnumerableType(Type type)
{
if (type == null)
{
return false;
}
if (type == typeof(string))
{
return false;
}
if (type.IsArray)
{
return true;
}
if (typeof(IEnumerable).IsAssignableFrom(type))
{
return true;
}
if (type.GetInterfaces().Any(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
return true;
}
return false;
}
private static Type UnwrapNullable(Type type)
{
Type? underlying = Nullable.GetUnderlyingType(type);
if (underlying != null)
{
return underlying;
}
return type;
}
}

View File

@@ -0,0 +1,24 @@
namespace CertMgr.Core.Storage;
public sealed class EmptyStorage : Storage
{
public static readonly IStorage Empty = new EmptyStorage();
private EmptyStorage()
{
}
public override bool CanWrite => true;
public override bool CanRead => true;
protected override Task<StoreResult> DoWriteAsync(Stream source, CancellationToken cancellationToken)
{
return Task.FromResult(StoreResult.CreateSuccess());
}
protected override Task<StoreResult> DoReadAsync(Stream target, CancellationToken cancellationToken)
{
return Task.FromResult(StoreResult.CreateSuccess());
}
}

View File

@@ -0,0 +1,44 @@
using CertMgr.Core.Utils;
namespace CertMgr.Core.Storage;
public sealed class FileStorage : Storage
{
private readonly string _fileFullPath;
private readonly FileStoreMode _mode;
public FileStorage(string fileFullPath, FileStoreMode mode)
{
_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))
{
await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
return StoreResult.CreateSuccess();
}
protected override async Task<StoreResult> DoReadAsync(Stream target, CancellationToken cancellationToken)
{
using (FileStream fs = new FileStream(_fileFullPath, (FileMode)_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);
}
}

View File

@@ -0,0 +1,16 @@
namespace CertMgr.Core.Storage;
public enum FileStoreMode
{
/// <summary>Create new file or append to existing.</summary>
AppendOrNew = FileMode.Append,
/// <summary>Create new file or overwrite existing.</summary>
OverwriteOrNew = FileMode.Create,
/// <summary>Create new file. Throw if already exists.</summary>
Create = FileMode.CreateNew,
/// <summary>Open existing file. Throw if doesn't exist.</summary>
Open = FileMode.Open,
}

View File

@@ -0,0 +1,12 @@
namespace CertMgr.Core.Storage;
public interface IStorage
{
Task<StoreResult> WriteAsync(Stream source, 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);
}

View File

@@ -0,0 +1,83 @@
using CertMgr.Core.Utils;
namespace CertMgr.Core.Storage;
public abstract class Storage : IStorage
{
public virtual bool CanRead => false;
public virtual bool CanWrite => false;
public async Task<StoreResult> WriteAsync(Stream source, CancellationToken cancellationToken)
{
if (!CanWrite)
{
throw new StorageException("Cannot write. Storage not writable");
}
try
{
return await DoWriteAsync(source, cancellationToken);
}
catch (Exception e)
{
StorageException ex = new StorageException(e, "Failed to write to storage of type '{0}' from stream of type '{1}'", GetType().ToString(false), source.GetType().ToString(false));
return StoreResult.CreateFailure(ex);
}
}
protected abstract Task<StoreResult> DoWriteAsync(Stream source, CancellationToken cancellationToken);
public async Task<StoreResult> ReadAsync(Stream target, CancellationToken cancellationToken)
{
if (!CanRead)
{
throw new StorageException("Cannot read. Storage not readable");
}
try
{
return await DoReadAsync(target, cancellationToken);
}
catch (Exception e)
{
StorageException ex = new StorageException(e, "Failed to read from storage of type '{0}' to stream of type '{1}'", GetType().ToString(false), target.GetType().ToString(false));
return StoreResult.CreateFailure(ex);
}
}
protected abstract Task<StoreResult> DoReadAsync(Stream target, CancellationToken cancellationToken);
/*public async Task<StoreResult> WriteFromAsync<TResult>(StorageAdapter<TResult> adapter, CancellationToken cancellationToken)
{
try
{
return await adapter.WriteAsync(this, cancellationToken).ConfigureAwait(false);
}
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);
}
}*/
/*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);
}
}*/
public override string ToString()
{
return string.Format("{0}: can-read = {1}, can-write = {2}", GetType().Name, CanRead ? "yes" : "no", CanWrite ? "yes" : "no");
}
}

View File

@@ -0,0 +1,15 @@
using CertMgr.Core.Utils;
namespace CertMgr.Core.Storage;
public class StorageException : Exception
{
internal StorageException(string messageFormat, params object?[] messageArgs)
: base(StringFormatter.Format(messageFormat, messageArgs))
{
}
internal StorageException(Exception? innerException, string messageFormat, params object?[] messageArgs)
: base(StringFormatter.Format(messageFormat, messageArgs), innerException)
{
}
}

View File

@@ -0,0 +1,6 @@
namespace CertMgr.Core.Storage;
public enum StorageType
{
File = 1
}

View File

@@ -0,0 +1,39 @@
using System.Diagnostics;
namespace CertMgr.Core.Storage;
public class StoreResult
{
private static readonly StoreResult Success = new StoreResult(true);
private static readonly StoreResult Failure = new StoreResult(false);
public StoreResult(bool isSuccess)
{
IsSuccess = isSuccess;
}
public StoreResult(bool isSuccess, Exception? exception)
{
IsSuccess = isSuccess;
Exception = exception;
}
public bool IsSuccess { [DebuggerStepThrough] get; }
public Exception? Exception { [DebuggerStepThrough] get; }
public static StoreResult CreateSuccess()
{
return Success;
}
public static StoreResult CreateFailure()
{
return Failure;
}
public static StoreResult CreateFailure(Exception? exception)
{
return exception != null ? new StoreResult(false, exception) : Failure;
}
}

View File

@@ -0,0 +1,30 @@
using System.Diagnostics;
namespace CertMgr.Core.Storage;
public sealed class StoreResult<TResult> : StoreResult
{
public StoreResult(TResult? result, bool isSuccess)
: base(isSuccess)
{
Result = result;
}
public StoreResult(TResult? result, bool isSuccess, Exception? exception)
: base(isSuccess, exception)
{
Result = result;
}
public TResult? Result { [DebuggerStepThrough] get; }
public static StoreResult<TResult> CreateFailure(TResult? result, Exception? exception)
{
return exception != null ? new StoreResult<TResult>(result, false, exception) : new StoreResult<TResult>(result, false);
}
public static StoreResult<TResult> CreateSuccess(TResult? result)
{
return new StoreResult<TResult>(result, true);
}
}

View File

@@ -0,0 +1,45 @@
namespace CertMgr.Core.Utils;
using System;
using System.Threading;
using System.Threading.Tasks;
public sealed class AsyncLock
{
private readonly SemaphoreSlim _semaphore;
private readonly Task<IDisposable> _disposer;
public AsyncLock()
{
_semaphore = new SemaphoreSlim(1, 1);
_disposer = Task.FromResult((IDisposable)new AsyncLockDisposer(this));
}
public Task<IDisposable> LockAsync(CancellationToken cancellation = default)
{
Task wait = _semaphore.WaitAsync();
if (wait.IsCompleted)
{
return _disposer;
}
else
{
return wait.ContinueWith((_, state) => (IDisposable)state!, _disposer.Result, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
}
private sealed class AsyncLockDisposer : IDisposable
{
private readonly AsyncLock _owner;
internal AsyncLockDisposer(AsyncLock owner)
{
_owner = owner;
}
public void Dispose()
{
_owner._semaphore.Release();
}
}
}

View File

@@ -0,0 +1,248 @@
using System.Reflection;
using System.Text;
namespace CertMgr.Core.Utils;
internal static class Extenders
{
private static readonly Dictionary<Type, string> _typeAliases = new Dictionary<Type, string>
{
{ typeof(long), "long" },
{ typeof(ulong), "ulong" },
{ typeof(int), "int" },
{ typeof(uint), "uint" },
{ typeof(short), "short" },
{ typeof(ushort), "ushort" },
{ typeof(byte), "byte" },
{ typeof(sbyte), "sbyte" },
{ typeof(double), "double" },
{ typeof(float), "float" },
{ typeof(decimal), "decimal" },
{ typeof(char), "char" },
{ typeof(string), "string" },
{ typeof(void), "void" },
{ typeof(bool), "bool" },
{ typeof(object), "object" },
};
public static string ToSeparatedList<T>(this IEnumerable<T> items, Func<T, string> formatter, string itemSeparator, string? lastItemSeparator = null)
{
using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped();
StringBuilder sb = lease.Builder;
ToSeparatedList(items, sb, formatter, itemSeparator, lastItemSeparator);
return sb.ToString();
}
public static void ToSeparatedList<T>(this IEnumerable<T> items, StringBuilder sb, Func<T, string> formatter, string itemSeparator, string? lastItemSeparator = null)
{
if (!items.Any())
{
return;
}
if (formatter == null)
{
formatter = item => item?.ToString() ?? "<null>";
}
lastItemSeparator = lastItemSeparator ?? itemSeparator;
using (IEnumerator<T> enu = items.GetEnumerator())
{
if (enu.MoveNext())
{
sb.Append(formatter(enu.Current));
bool hasNext = enu.MoveNext();
while (hasNext)
{
T current = enu.Current;
hasNext = enu.MoveNext();
sb.Append(hasNext ? itemSeparator : lastItemSeparator);
sb.Append(formatter(current));
}
}
}
}
public static string ToString(this Type type, bool includeNamespace)
{
if (type == null)
{
return null;
}
using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped();
StringBuilder sb = lease.Builder;
type.AppendStringType(sb, includeNamespace);
string typeString = sb.ToString();
return typeString;
}
private static void AppendStringType(this Type type, StringBuilder sb, bool includeNamespace)
{
if (type == null)
{
return;
}
if (type.IsGenericType)
{
if (includeNamespace)
{
sb.Append(type.Namespace);
sb.Append('.');
}
sb.Append(type.Name.Substring(0, type.Name.IndexOf('`')));
sb.Append('<');
Type[] genericArguments = type.GenericTypeArguments;
for (int i = 0; i < genericArguments.Length; i++)
{
if (i > 0)
{
sb.Append(',');
}
genericArguments[i].AppendStringType(sb, includeNamespace);
}
sb.Append('>');
}
else
{
if (_typeAliases.TryGetValue(type, out string? alias))
{
sb.Append(alias);
}
else
{
if (includeNamespace)
{
sb.Append(type.Namespace);
sb.Append('.');
}
sb.Append(type.Name);
}
}
}
public static bool Equivalent<T>(this IEnumerable<T?> left, IEnumerable<T?> right, IEqualityComparer<T?>? comparer = null)
{
if (ReferenceEquals(left, right))
{
return true;
}
if (left == null || right == null)
{
return false;
}
if (left.TryGetCount(out int lcount) && right.TryGetCount(out int rcount))
{
if (lcount != rcount)
{
return false;
}
}
int nullCount = 0;
Dictionary<T, int> cnt = new Dictionary<T, int>(comparer ?? EqualityComparer<T?>.Default);
foreach (T? item in left)
{
if (item == null)
{
nullCount++;
continue;
}
if (cnt.TryGetValue(item, out int value))
{
cnt[item] = ++value;
}
else
{
cnt.Add(item, 1);
}
}
foreach (T? item in right)
{
if (item == null)
{
nullCount--;
continue;
}
if (cnt.ContainsKey(item))
{
cnt[item]--;
}
else
{
return false;
}
}
bool equals = nullCount == 0 && cnt.Values.All(c => c == 0);
return equals;
}
private static bool TryGetCount<T>(this IEnumerable<T> items, out int count)
{
count = -1;
bool success = false;
if (items is ICollection<T> colt)
{
count = colt.Count;
success = true;
}
else if (items is System.Collections.ICollection col)
{
count = col.Count;
success = true;
}
else if (items is IReadOnlyCollection<T> rocol)
{
count = rocol.Count;
success = true;
}
return success;
}
public static bool IsAnyOf<T>(this T value, params T[] availableValues) where T : struct
{
if (!typeof(T).IsEnum)
{
throw new Exception(string.Format("Expected that the type is enum, got '{0}'", typeof(T).ToString(false)));
}
bool isOneOf = false;
Attribute? flags = typeof(T).GetCustomAttribute(typeof(FlagsAttribute));
if (flags == null)
{
if (availableValues != null && availableValues.Length > 0)
{
isOneOf = availableValues.Any(av => value.Equals(av));
}
}
else
{
ulong v = Convert.ToUInt64(value);
foreach (T availableValue in availableValues)
{
ulong av = Convert.ToUInt64(availableValue);
if ((v & av) == v)
{
isOneOf = true;
break;
}
}
}
return isOneOf;
}
}

View File

@@ -0,0 +1,17 @@
namespace CertMgr.Core.Utils;
public enum MachineNameFormat
{
/// <summary>NetBIOS name of the local computer. Limited to 15 characters and may be a truncated version of DNS host name.</summary>
NetBios = 1,
/// <summary>DNS name of the local computer.</summary>
Hostname = 2,
/// <summary>Fully qualified DNS name of the local computer. Combination of DNS hostname and DNS domain using form &lt;HostName&gt;.&lt;DomainName&gt;</summary>
FullyQualified = 3,
/// <summary>Name of the DNS domain assigned to the local computer.</summary>
Domain = 4
}

View File

@@ -0,0 +1,37 @@
using System.Net;
using System.Net.NetworkInformation;
using CertMgr.Core.Exceptions;
namespace CertMgr.Core.Utils;
public static class NetUtils
{
/// <summary>Returns DNS name of the local computer.</summary>
public static string MachineName => GetMachineName(MachineNameFormat.Hostname);
public static string GetMachineName(MachineNameFormat format)
{
string name;
switch (format)
{
case MachineNameFormat.NetBios:
name = Environment.MachineName;
break;
case MachineNameFormat.Hostname:
name = Dns.GetHostName();
break;
case MachineNameFormat.FullyQualified:
name = Dns.GetHostEntry("localhost").HostName;
break;
case MachineNameFormat.Domain:
name = IPGlobalProperties.GetIPGlobalProperties().DomainName;
break;
default:
throw new UnsupportedValueException(format);
}
return name;
}
}

View File

@@ -0,0 +1,172 @@
using System.Collections.Concurrent;
using System.Text;
namespace CertMgr.Core.Utils;
internal static class StringBuilderCache
{
private static readonly ConcurrentStack<StringBuilder> _cache = new ConcurrentStack<StringBuilder>();
private static readonly int _maxCapacityOfCachedItem = 8;
private static readonly int _maxCountOfItemsInCache = 4 * 1024;
private static int _pooledCount;
[ThreadStatic]
private static StringBuilder? _hotSlot;
public static StringBuilder Acquire(int initialCapacity = 16)
{
StringBuilder? sb = Interlocked.Exchange(ref _hotSlot, null);
if (sb != null)
{
Prepare(sb, initialCapacity);
return sb;
}
if (_cache.TryPop(out sb))
{
Interlocked.Decrement(ref _pooledCount);
Prepare(sb, initialCapacity);
return sb;
}
return new StringBuilder(initialCapacity);
}
public static ScopedBuilder AcquireScoped(int initialCapacity = 16)
{
return new ScopedBuilder(Acquire(initialCapacity));
}
public static void Release(StringBuilder sb)
{
if (sb == null)
{
return;
}
if (sb.Capacity > _maxCountOfItemsInCache)
{
return;
}
sb.Clear();
if (Interlocked.CompareExchange(ref _hotSlot, sb, null) == null)
{
return;
}
int newCount = Interlocked.Increment(ref _pooledCount);
if (newCount <= _maxCapacityOfCachedItem)
{
_cache.Push(sb);
return;
}
Interlocked.Decrement(ref _pooledCount);
}
public static string GetStringAndRelease(StringBuilder sb)
{
string s = sb.ToString();
Release(sb);
return s;
}
private static void Prepare(StringBuilder sb, int minCapacity)
{
sb.Clear();
if (sb.Capacity < minCapacity)
{
sb.EnsureCapacity(minCapacity);
}
}
public sealed class ScopedBuilder : IDisposable, IAsyncDisposable
{
private StringBuilder _sb;
private int _disposed;
internal ScopedBuilder(StringBuilder sb)
{
_sb = sb;
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) == 0)
{
StringBuilder? sb = _sb;
_sb = null;
Release(sb);
}
}
public ValueTask DisposeAsync()
{
Dispose();
return new ValueTask();
}
public string GetStringAndRelease()
{
if (Interlocked.CompareExchange(ref _disposed, 1, 1) == 1)
{
throw new ObjectDisposedException(string.Format("Cannot get string from this instance of StringBuilderCache as it has already been disposed"));
}
string s = _sb.ToString();
Dispose();
return s;
}
public StringBuilder Builder => _sb;
public static implicit operator StringBuilder(ScopedBuilder b)
{
return b.Builder;
}
}
}
/*internal static class StringBuilderCache
{
internal const int MaxBuilderSize = 360;
internal const int DefaultCapacity = 16;
[ThreadStatic]
private static StringBuilder? CachedInstance;
public static StringBuilder Acquire(int capacity = DefaultCapacity)
{
if (capacity <= 360)
{
StringBuilder? cachedInstance = CachedInstance;
if (cachedInstance != null && capacity <= cachedInstance.Capacity)
{
CachedInstance = null;
cachedInstance.Clear();
return cachedInstance;
}
}
return new StringBuilder(capacity);
}
public static void Release(StringBuilder sb)
{
if (sb.Capacity <= 360)
{
CachedInstance = sb;
}
}
public static string GetStringAndRelease(StringBuilder sb)
{
string result = sb.ToString();
Release(sb);
return result;
}
}*/

View File

@@ -0,0 +1,30 @@
using System.Text;
namespace CertMgr.Core.Utils;
internal static class StringFormatter
{
public static string Format(string messageFormat, params object?[] messageArgs)
{
try
{
return string.Format(messageFormat, messageArgs);
}
catch (Exception e)
{
using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped();
StringBuilder sb = lease.Builder;
sb.AppendFormat("Failed to format message: '{0}'. Arguments (count = {1}) = ", messageFormat, messageArgs?.Length ?? -1);
if (messageArgs == null)
{
sb.Append("<null>");
}
else
{
messageArgs.ToSeparatedList(sb, obj => obj?.ToString() ?? "<null>", ",");
}
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Diagnostics;
namespace CertMgr.Core.Validation;
public interface ISettingValidator
{
string SettingName { [DebuggerStepThrough] get; }
Task<ValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,6 @@
namespace CertMgr.Core.Validation;
public interface ISettingValidator<T> : ISettingValidator
{
Task<ValidationResult> ValidateAsync(T? settingValue, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,27 @@
using System.Diagnostics;
namespace CertMgr.Core.Validation;
public sealed class StringValidator
{
public sealed class IsNotNull : ISettingValidator<string>
{
public IsNotNull(string settingName)
{
SettingName = settingName;
}
public string SettingName { [DebuggerStepThrough] get; }
public Task<ValidationResult> ValidateAsync(string? value, CancellationToken cancellationToken)
{
ValidationResult result = new ValidationResult(SettingName, value != null, "value is null");
return Task.FromResult(result);
}
public Task<ValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken)
{
return ValidateAsync(value as string, cancellationToken);
}
}
}

View File

@@ -0,0 +1,26 @@
using System.Diagnostics;
using CertMgr.Core.Utils;
namespace CertMgr.Core.Validation;
public sealed class ValidationResult
{
public ValidationResult(string propertyName, bool isValid, string justificationFormat, params object?[] justificationArgs)
{
PropertyName = propertyName;
Justification = StringFormatter.Format(justificationFormat, justificationArgs);
IsValid = isValid;
}
public string PropertyName { [DebuggerStepThrough] get; }
public string Justification { [DebuggerStepThrough] get; }
public bool IsValid { [DebuggerStepThrough] get; }
public override string ToString()
{
return string.Format("{0}: {1} => {2}", PropertyName, IsValid ? "valid" : "not valid", Justification);
}
}

View File

@@ -0,0 +1,52 @@
using System.Collections;
namespace CertMgr.Core.Validation;
public sealed class ValidationResults : IReadOnlyCollection<ValidationResult>
{
private readonly List<ValidationResult> _results;
internal ValidationResults()
{
_results = new List<ValidationResult>();
}
public int Count => _results.Count;
public bool IsValid => _results.All(res => res.IsValid);
internal void Add(ValidationResult result)
{
_results.Add(result);
}
internal void Add(IReadOnlyCollection<ValidationResult> results)
{
_results.AddRange(results);
}
internal void AddValid(string propertyName, string justificationFormat, params object?[] justificationArgs)
{
Add(new ValidationResult(propertyName, true, justificationFormat, justificationArgs));
}
internal void AddInvalid(string propertyName, string justificationFormat, params object?[] justificationArgs)
{
Add(new ValidationResult(propertyName, false, justificationFormat, justificationArgs));
}
public IEnumerator<ValidationResult> GetEnumerator()
{
return _results.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public override string ToString()
{
return string.Format("is-valid = {0}, total-count = {1}, invalid-count = {2}", IsValid ? "yes" : "no", _results.Count, _results.Where(r => !r.IsValid).Count());
}
}

View File

@@ -0,0 +1,90 @@
using System.Diagnostics;
using CertMgr.CertGen;
using CertMgr.Core.Attributes;
using CertMgr.Core.Converters.Impl;
using CertMgr.Core.Jobs;
using CertMgr.Core.Storage;
using CertMgr.Core.Validation;
namespace CertMgr.Jobs;
public sealed class CertificateSettings : JobSettings
{
public CertificateSettings()
{
Algorithm = CertificateAlgorithm.ECDsa;
Curve = EcdsaCurve.P384;
HashAlgorithm = CertGen.HashAlgorithm.Sha384;
ValidityPeriod = TimeSpan.FromDays(365);
}
[Setting("subject", IsMandatory = true, Validator = typeof(StringValidator.IsNotNull))]
public string? Subject { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("subject-alternate-name", AlternateNames = ["san"])]
public IReadOnlyCollection<string>? SubjectAlternateNames { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("algorithm", Default = CertificateAlgorithm.ECDsa, Converter = typeof(EnumConverter))]
public CertificateAlgorithm? Algorithm { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("ecdsa-curve")]
public EcdsaCurve? Curve { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("rsa-key-size")]
public RsaKeySize? RsaKeySize { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("hash-algorithm", AlternateNames = ["ha"])]
public HashAlgorithm? HashAlgorithm { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("is-certificate-authority", Default = false, AlternateNames = ["isca"])]
public bool IsCertificateAuthority { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("issuer", Converter = typeof(StorageConverter))]
public IStorage? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("issuer-password")]
public string? IssuerPassword { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("storage", IsMandatory = true, Converter = typeof(StorageConverter))]
public IStorage? Storage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("password")]
public string? Password { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("validity-period", Default = "365d", Converter = typeof(TimeSpanConverter))]
public TimeSpan? ValidityPeriod { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
protected override Task DoValidateAsync(ValidationResults results, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(Subject))
{
results.AddInvalid(nameof(Subject), "must not be empty");
}
if (Algorithm == CertificateAlgorithm.ECDsa)
{
if (!Curve.HasValue || !Enum.IsDefined(Curve.Value))
{
results.AddInvalid(nameof(Curve), "valid value must be specified: '{0}'", Curve?.ToString() ?? "<null>");
}
}
else if (Algorithm == CertificateAlgorithm.RSA)
{
if (!RsaKeySize.HasValue || !Enum.IsDefined(RsaKeySize.Value))
{
results.AddInvalid(nameof(RsaKeySize), "value value must be specified: '{0}'", RsaKeySize?.ToString() ?? "<null>");
}
}
if (!HashAlgorithm.HasValue || !Enum.IsDefined(HashAlgorithm.Value))
{
results.AddInvalid(nameof(HashAlgorithm), "value value must be specified: '{0}'", HashAlgorithm?.ToString() ?? "<null>");
}
if (Storage == null)
{
results.AddInvalid(nameof(Storage), "must be specified");
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,95 @@
using System.Security.Cryptography.X509Certificates;
using CertMgr.CertGen;
using CertMgr.Core.Exceptions;
using CertMgr.Core.Jobs;
using CertMgr.Core.Log;
using CertMgr.Core.Storage;
using CertMgr.Core.Utils;
namespace CertMgr.Jobs;
public sealed class CreateCertificateJob : Job<CertificateSettings>
{
public const string ID = "create-certificate";
protected override async Task<JobResult> DoExecuteAsync(CancellationToken cancellationToken)
{
CertificateSettings cs = Settings;
CLog.Info("creating certificate using settings: subject = '{0}', algorithm = '{1}', curve = '{2}'", cs.Subject, cs.Algorithm?.ToString() ?? "<null>", cs.Curve);
GeneratorSettings gs = CreateGeneratorSettings();
CertGen.CertificateSettings cgcs = await CreateCertificateSettingsAsync(cancellationToken).ConfigureAwait(false);
CertificateManager cm = new CertificateManager();
using (X509Certificate2 cert = await cm.CreateAsync(cgcs, gs, cancellationToken).ConfigureAwait(false))
{
if (Settings.Storage != null)
{
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);
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());
}
}
}
}
return CreateSuccess("Certificate was successfully created");
}
private GeneratorSettings CreateGeneratorSettings()
{
GeneratorSettings gs;
switch (Settings.Algorithm)
{
case CertificateAlgorithm.ECDsa:
gs = new EcdsaGeneratorSettings(Settings.Curve.Value);
break;
case CertificateAlgorithm.RSA:
gs = new RsaGeneratorSettings(Settings.RsaKeySize.Value);
break;
default:
throw new UnsupportedValueException(Settings.Algorithm);
}
return gs;
}
private async Task<CertGen.CertificateSettings> CreateCertificateSettingsAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
CertGen.CertificateSettings cgcs = new CertGen.CertificateSettings();
X509KeyStorageFlags flags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet;
cgcs.ValidityPeriod = Settings.ValidityPeriod.HasValue ? Settings.ValidityPeriod.Value : TimeSpan.FromDays(365);
cgcs.ExportableKeys = true;
cgcs.FriendlyName = "I'm your friend";
if (Settings.Issuer != null)
{
using (MemoryStream ms = new MemoryStream())
{
await Settings.Issuer.ReadAsync(ms, cancellationToken).ConfigureAwait(false);
cgcs.Issuer = X509CertificateLoader.LoadPkcs12(ms.GetBuffer(), Settings.IssuerPassword, flags);
}
}
cgcs.SubjectName = Settings.Subject;
if (Settings.SubjectAlternateNames != null)
{
foreach (string altName in Settings.SubjectAlternateNames)
{
cgcs.SubjectAlternateNames.Add(altName);
}
}
return cgcs;
}
}

17
certmgr/Program.cs Normal file
View File

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

12
certmgr/certmgr.csproj Normal file
View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<BaseOutputPath>..\BuildOutput\bin</BaseOutputPath>
<RootNamespace>CertMgr</RootNamespace>
</PropertyGroup>
</Project>