some fixes
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
.vs
|
.vs
|
||||||
certmgr/obj/*
|
certmgr/obj/*
|
||||||
certmgr/Obsolete/*
|
certmgr/Obsolete/*
|
||||||
|
certmgrTest/obj/*
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,10 +1,12 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.14.36511.14 d17.14
|
VisualStudioVersion = 17.14.36511.14
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "certmgr", "certmgr\certmgr.csproj", "{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "certmgr", "certmgr\certmgr.csproj", "{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "certmgrTest", "certmgrTest\certmgrTest.csproj", "{D6D02CCB-DA1F-4575-9422-FCD632B782B3}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -15,6 +17,10 @@ Global
|
|||||||
{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}.Release|Any CPU.Build.0 = Release|Any CPU
|
{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D6D02CCB-DA1F-4575-9422-FCD632B782B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D6D02CCB-DA1F-4575-9422-FCD632B782B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D6D02CCB-DA1F-4575-9422-FCD632B782B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D6D02CCB-DA1F-4575-9422-FCD632B782B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Net;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
@@ -10,8 +11,9 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
|||||||
where TAlgorithm : AsymmetricAlgorithm
|
where TAlgorithm : AsymmetricAlgorithm
|
||||||
where TSettings : GeneratorSettings
|
where TSettings : GeneratorSettings
|
||||||
{
|
{
|
||||||
internal CertificateGeneratorBase(TSettings settings)
|
internal CertificateGeneratorBase(GeneratorType type, TSettings settings)
|
||||||
{
|
{
|
||||||
|
Type = type;
|
||||||
Settings = settings;
|
Settings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,6 +22,8 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
|||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GeneratorType Type { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
protected TSettings Settings { [DebuggerStepThrough] get; }
|
protected TSettings Settings { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
public async Task<X509Certificate2> CreateAsync(CertificateSettings settings, CancellationToken cancellationToken)
|
public async Task<X509Certificate2> CreateAsync(CertificateSettings settings, CancellationToken cancellationToken)
|
||||||
@@ -30,8 +34,6 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
|||||||
return cert;
|
return cert;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract bool IsEphemeral(TAlgorithm key);
|
|
||||||
|
|
||||||
protected virtual void ValidateSettings(CertificateSettings settings)
|
protected virtual void ValidateSettings(CertificateSettings settings)
|
||||||
{
|
{
|
||||||
if (settings.Issuer != null)
|
if (settings.Issuer != null)
|
||||||
@@ -71,37 +73,12 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
|||||||
|
|
||||||
protected abstract X509Certificate2 JoinPrivateKey(X509Certificate2 publicOnlyCert, TAlgorithm privateKey);
|
protected abstract X509Certificate2 JoinPrivateKey(X509Certificate2 publicOnlyCert, TAlgorithm privateKey);
|
||||||
|
|
||||||
protected abstract string? GetContainerUniqueName(TAlgorithm privateKey);
|
|
||||||
|
|
||||||
protected abstract TAlgorithm? GetPrivateKey(X509Certificate2 cert);
|
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)
|
private Task<X509Certificate2> CreateInternalAsync(CertificateSettings settings, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
X509Certificate2 cert;
|
X509Certificate2 cert;
|
||||||
|
|
||||||
using (TAlgorithm privateKey = CreatePrivateKey())
|
using (TAlgorithm privateKey = CreatePrivateKey())
|
||||||
@@ -128,6 +105,50 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
|||||||
return Task.FromResult(cert);
|
return Task.FromResult(cert);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CertificateRequest CreateRequest(CertificateSettings settings, TAlgorithm privateKey)
|
||||||
|
{
|
||||||
|
string commonName = CreateCommonName(settings.SubjectName);
|
||||||
|
CertificateRequest request = DoCreateRequest(commonName, privateKey);
|
||||||
|
|
||||||
|
SubjectAlternativeNameBuilder altNames = new SubjectAlternativeNameBuilder();
|
||||||
|
string subj = commonName.Substring("CN=".Length);
|
||||||
|
if (!settings.SubjectAlternateNames.Contains(new SubjectAlternateName(SANKind.DNS, subj)))
|
||||||
|
{
|
||||||
|
altNames.AddDnsName(subj);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (SubjectAlternateName altName in settings.SubjectAlternateNames)
|
||||||
|
{
|
||||||
|
switch (altName.Kind)
|
||||||
|
{
|
||||||
|
case SANKind.DNS:
|
||||||
|
altNames.AddDnsName(altName.Value);
|
||||||
|
break;
|
||||||
|
case SANKind.IP:
|
||||||
|
if (IPAddress.TryParse(altName.Value, out IPAddress? ipAddress))
|
||||||
|
{
|
||||||
|
altNames.AddIpAddress(ipAddress);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedValueException(altName.Kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 X509SignatureGenerator GetSignatureGenerator(X509Certificate2 issuerCertificate)
|
private X509SignatureGenerator GetSignatureGenerator(X509Certificate2 issuerCertificate)
|
||||||
{
|
{
|
||||||
X509SignatureGenerator? sgen = null;
|
X509SignatureGenerator? sgen = null;
|
||||||
@@ -183,7 +204,7 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
|||||||
|
|
||||||
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
|
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
|
||||||
{
|
{
|
||||||
rng.GetBytes(serial);
|
rng.GetNonZeroBytes(serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
return serial;
|
return serial;
|
||||||
@@ -212,6 +233,6 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Format("Type = {0}", this is RsaCertificateGenerator ? "RSA" : this is EcdsaCertificateGenerator ? "ECDSA" : "<unknown>");
|
return string.Format("Type = {0}", Type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public sealed class CertificateSettings
|
|||||||
{
|
{
|
||||||
internal CertificateSettings()
|
internal CertificateSettings()
|
||||||
{
|
{
|
||||||
SubjectAlternateNames = new SubjectAlternateNames([NetUtils.MachineName]);
|
SubjectAlternateNames = new SubjectAlternateNames();
|
||||||
|
|
||||||
SubjectName = NetUtils.MachineName;
|
SubjectName = NetUtils.MachineName;
|
||||||
Issuer = null;
|
Issuer = null;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace CertMgr.CertGen;
|
|||||||
internal sealed class EcdsaCertificateGenerator : CertificateGeneratorBase<ECDsa, EcdsaGeneratorSettings>
|
internal sealed class EcdsaCertificateGenerator : CertificateGeneratorBase<ECDsa, EcdsaGeneratorSettings>
|
||||||
{
|
{
|
||||||
internal EcdsaCertificateGenerator(EcdsaGeneratorSettings settings)
|
internal EcdsaCertificateGenerator(EcdsaGeneratorSettings settings)
|
||||||
: base(settings)
|
: base(GeneratorType.Ecdsa, settings)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,16 +33,6 @@ internal sealed class EcdsaCertificateGenerator : CertificateGeneratorBase<ECDsa
|
|||||||
return privateKey;
|
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()
|
private ECCurve GetCurve()
|
||||||
{
|
{
|
||||||
ECCurve curve;
|
ECCurve curve;
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
public enum GeneratorType
|
public enum GeneratorType
|
||||||
{
|
{
|
||||||
Ecdsa = 1,
|
Ecdsa = 1,
|
||||||
Rsa
|
RSA
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace CertMgr.CertGen;
|
|||||||
internal sealed class RsaCertificateGenerator : CertificateGeneratorBase<RSA, RsaGeneratorSettings>
|
internal sealed class RsaCertificateGenerator : CertificateGeneratorBase<RSA, RsaGeneratorSettings>
|
||||||
{
|
{
|
||||||
internal RsaCertificateGenerator(RsaGeneratorSettings settings)
|
internal RsaCertificateGenerator(RsaGeneratorSettings settings)
|
||||||
: base(settings)
|
: base(GeneratorType.RSA, settings)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,16 +33,6 @@ internal sealed class RsaCertificateGenerator : CertificateGeneratorBase<RSA, Rs
|
|||||||
return privateKey;
|
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()
|
private int GetKeySize()
|
||||||
{
|
{
|
||||||
int keySize;
|
int keySize;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace CertMgr.CertGen;
|
|||||||
public sealed class RsaGeneratorSettings : GeneratorSettings
|
public sealed class RsaGeneratorSettings : GeneratorSettings
|
||||||
{
|
{
|
||||||
public RsaGeneratorSettings(RsaKeySize keySize)
|
public RsaGeneratorSettings(RsaKeySize keySize)
|
||||||
: base(GeneratorType.Rsa)
|
: base(GeneratorType.RSA)
|
||||||
{
|
{
|
||||||
KeySize = keySize;
|
KeySize = keySize;
|
||||||
|
|
||||||
|
|||||||
7
certmgr/CertGen/SANKind.cs
Normal file
7
certmgr/CertGen/SANKind.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public enum SANKind
|
||||||
|
{
|
||||||
|
DNS = 1,
|
||||||
|
IP
|
||||||
|
}
|
||||||
57
certmgr/CertGen/SubjectAlternateName.cs
Normal file
57
certmgr/CertGen/SubjectAlternateName.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public sealed class SubjectAlternateName : IEquatable<SubjectAlternateName>
|
||||||
|
{
|
||||||
|
public SubjectAlternateName(SANKind kind, string value)
|
||||||
|
{
|
||||||
|
Kind = kind;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SubjectAlternateName(SANKind kind, ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
Kind = kind;
|
||||||
|
Value = value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SANKind Kind { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public string Value { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("{0}: '{1}'", Kind, Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return (Kind, Value).GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
return Equals(obj as SubjectAlternateName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(SubjectAlternateName? other)
|
||||||
|
{
|
||||||
|
if (other is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Kind != other.Kind)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,119 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
|
||||||
|
using CertMgr.Core.Log;
|
||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
namespace CertMgr.CertGen;
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
public sealed class SubjectAlternateNames : IReadOnlyCollection<string>
|
public sealed class SubjectAlternateNames : IReadOnlyCollection<SubjectAlternateName>
|
||||||
{
|
{
|
||||||
private readonly HashSet<string> _items;
|
private readonly HashSet<SubjectAlternateName> _items;
|
||||||
|
|
||||||
public SubjectAlternateNames()
|
public SubjectAlternateNames()
|
||||||
{
|
{
|
||||||
_items = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
_items = new HashSet<SubjectAlternateName>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SubjectAlternateNames(IReadOnlyCollection<string> items)
|
public SubjectAlternateNames(IReadOnlyCollection<string> items)
|
||||||
{
|
{
|
||||||
_items = new HashSet<string>(items, StringComparer.OrdinalIgnoreCase);
|
_items = new HashSet<SubjectAlternateName>();
|
||||||
|
foreach (string item in items)
|
||||||
|
{
|
||||||
|
Add(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Count => _items.Count;
|
public int Count => _items.Count;
|
||||||
|
|
||||||
public bool Add(string name)
|
public bool Add(string name)
|
||||||
{
|
{
|
||||||
bool added = _items.Add(name);
|
bool added = false;
|
||||||
|
|
||||||
|
// any kind of value except for DNS must be prefixed with its kind,
|
||||||
|
// e.g. "IP:w.x.y.z" or "IP:2001:db8::1" or "DNS:example.com"
|
||||||
|
// if not prefixed then it must be DNS
|
||||||
|
|
||||||
|
ReadOnlySpan<char> span = name.AsSpan();
|
||||||
|
if (span.StartsWith("DNS:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
added = AddDnsName(span);
|
||||||
|
}
|
||||||
|
else if (span.StartsWith("IP:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
added = AddIPAddress(span);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// fallback to dns name as other alt-names (UPN, URI, email..) are not supported
|
||||||
|
added = AddDnsName(span);
|
||||||
|
}
|
||||||
|
|
||||||
return added;
|
return added;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerator<string> GetEnumerator()
|
public bool AddDnsName(string name)
|
||||||
|
{
|
||||||
|
bool added = AddDnsName(name.AsSpan());
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AddDnsName(ReadOnlySpan<char> name)
|
||||||
|
{
|
||||||
|
bool added = false;
|
||||||
|
|
||||||
|
ReadOnlySpan<char> span = name;
|
||||||
|
if (span.StartsWith("DNS:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
span = span.Slice(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NetUtils.IsValidDns(span))
|
||||||
|
{
|
||||||
|
added = _items.Add(new SubjectAlternateName(SANKind.DNS, span));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CLog.Error("{0}: Value '{1}' is not valid DNS name", nameof(SubjectAlternateNames), name.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AddIPAddress(string ip)
|
||||||
|
{
|
||||||
|
bool added = AddIPAddress(ip.AsSpan());
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AddIPAddress(ReadOnlySpan<char> ip)
|
||||||
|
{
|
||||||
|
bool added = false;
|
||||||
|
|
||||||
|
ReadOnlySpan<char> span = ip;
|
||||||
|
if (span.StartsWith("IP:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
span = span.Slice(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NetUtils.IsValidIPAny(span))
|
||||||
|
{
|
||||||
|
added = _items.Add(new SubjectAlternateName(SANKind.IP, span));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CLog.Error("{0}: Value '{1}' is not valid IP address", nameof(SubjectAlternateNames), ip.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(SubjectAlternateName name)
|
||||||
|
{
|
||||||
|
bool contains = _items.Contains(name);
|
||||||
|
return contains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<SubjectAlternateName> GetEnumerator()
|
||||||
{
|
{
|
||||||
return _items.GetEnumerator();
|
return _items.GetEnumerator();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
/*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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
40
certmgr/CertGen/Utils/SubjectValidator.cs
Normal file
40
certmgr/CertGen/Utils/SubjectValidator.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
using CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen.Utils;
|
||||||
|
|
||||||
|
public sealed class SubjectValidator : ISettingValidator<string>
|
||||||
|
{
|
||||||
|
public SubjectValidator(string settingName)
|
||||||
|
{
|
||||||
|
SettingName = settingName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SettingName { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public Task<ValidationResult> ValidateAsync(string? settingValue, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(settingValue))
|
||||||
|
{
|
||||||
|
return Task.FromResult(new ValidationResult(SettingName, false, "must not be null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
X500DistinguishedName dn = new X500DistinguishedName(settingValue);
|
||||||
|
|
||||||
|
return Task.FromResult(new ValidationResult(SettingName, true, "success"));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new ValidationResult(SettingName, false, "invalid value: '{0}'. Exception = {1}: {2}", settingValue ?? "<null>", e.GetType().Name, e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return ValidateAsync(value as string, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,36 +8,26 @@ public sealed class EnumConverter : ValueConverter<Enum>
|
|||||||
{
|
{
|
||||||
Type resultType;
|
Type resultType;
|
||||||
|
|
||||||
bool isNullable = targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>);
|
Type? underlying = Nullable.GetUnderlyingType(targetType);
|
||||||
if (isNullable)
|
if (underlying != null)
|
||||||
{
|
{
|
||||||
Type[] args = targetType.GetGenericArguments();
|
resultType = underlying;
|
||||||
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
|
else
|
||||||
{
|
{
|
||||||
if (!targetType.IsEnum)
|
resultType = targetType;
|
||||||
{
|
|
||||||
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);
|
if (!resultType.IsEnum)
|
||||||
return Task.FromResult((Enum?)result);
|
{
|
||||||
|
throw new ConverterException("Cannot convert type '{0}' as enum as it is not enum or nullable", targetType.ToString(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Enum.TryParse(resultType, rawValue, true, out object? result) && Enum.IsDefined(resultType, result))
|
||||||
|
{
|
||||||
|
return Task.FromResult((Enum?)result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult((Enum?)null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@@ -25,62 +26,138 @@ internal sealed class SettingsBuilder
|
|||||||
_settingsType = settingsType;
|
_settingsType = settingsType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<Type> GetValidatedTypes(Type validatorType)
|
||||||
|
{
|
||||||
|
if (validatorType == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(validatorType));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Type> hits = new List<Type>();
|
||||||
|
|
||||||
|
if (validatorType.IsInterface && validatorType.IsGenericType && validatorType.GetGenericTypeDefinition() == typeof(ISettingValidator<>))
|
||||||
|
{
|
||||||
|
Type candidate = validatorType.GetGenericArguments()[0];
|
||||||
|
hits.Add(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Type iface in validatorType.GetInterfaces())
|
||||||
|
{
|
||||||
|
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(ISettingValidator<>))
|
||||||
|
{
|
||||||
|
Type candidate = iface.GetGenericArguments()[0];
|
||||||
|
if (!hits.Contains(candidate))
|
||||||
|
{
|
||||||
|
hits.Add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<AsyncResult<object?>> SetPropertyValueAsync(JobSettings settings, PropertyInfo propertyInfo, TypeInfo propertyType, SettingAttribute settingAttribute, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
object? convertedValue = null;
|
||||||
|
bool valueSet = false;
|
||||||
|
|
||||||
|
if (TryGetRawArgument(settingAttribute, out RawArgument? rawArg))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AsyncResult<object?> conversionResult = await ConvertRawValueAsync(settingAttribute, rawArg, propertyType.IsCollection, propertyInfo.PropertyType, propertyType.ElementType, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (conversionResult.IsSuccess)
|
||||||
|
{
|
||||||
|
propertyInfo.SetValue(settings, conversionResult.Value);
|
||||||
|
convertedValue = conversionResult.Value;
|
||||||
|
valueSet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
CLog.Error(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
||||||
|
throw new CertMgrException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (settingAttribute.IsMandatory)
|
||||||
|
{
|
||||||
|
ValidationResult valres = new ValidationResult(settingAttribute.Name, false, "Mandatory argument is missing");
|
||||||
|
settings.ValidationResults.Add(valres);
|
||||||
|
CLog.Error("mandatory argument '{0}' is missing", settingAttribute.Name);
|
||||||
|
}
|
||||||
|
else if (settingAttribute.Default != null)
|
||||||
|
{
|
||||||
|
TypeInfo typeInfo = UnwrapCollection(propertyInfo.PropertyType);
|
||||||
|
if (settingAttribute.Default.GetType() == typeInfo.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().ToString(false) ?? "<null>", typeInfo.ElementType.ToString(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return new AsyncResult<object?>(valueSet, convertedValue);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<JobSettings> LoadAsync(CancellationToken cancellationToken)
|
public async Task<JobSettings> LoadAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
JobSettings settings = CreateSettingsInstance();
|
JobSettings settings = CreateSettingsInstance();
|
||||||
|
|
||||||
foreach ((PropertyInfo propertyInfo, SettingAttribute settingAttribute) in GetPropertiesWithSettingAttribute())
|
foreach ((PropertyInfo propertyInfo, SettingAttribute settingAttribute) in GetPropertiesWithSettingAttribute())
|
||||||
{
|
{
|
||||||
if (TryGetRawArgument(settingAttribute, out RawArgument? rawArg))
|
TypeInfo propertyType = UnwrapCollection(propertyInfo.PropertyType);
|
||||||
{
|
|
||||||
(bool isCollection, Type elementType) = GetValueType(propertyInfo);
|
|
||||||
|
|
||||||
|
AsyncResult<object?> setPropertyResult = await SetPropertyValueAsync(settings, propertyInfo, propertyType, settingAttribute, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (setPropertyResult.IsSuccess)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
(bool converted, object? convertedValue) = await ConvertRawValueAsync(settingAttribute, rawArg, isCollection, propertyInfo.PropertyType, elementType, cancellationToken).ConfigureAwait(false);
|
if (settingAttribute.Validator != null)
|
||||||
if (converted)
|
|
||||||
{
|
{
|
||||||
propertyInfo.SetValue(settings, convertedValue);
|
IReadOnlyList<Type> validatedTypes = GetValidatedTypes(settingAttribute.Validator);
|
||||||
|
|
||||||
if (settingAttribute.Validator != null)
|
ISettingValidator? validatorInst = (ISettingValidator?)Activator.CreateInstance(settingAttribute.Validator, [settingAttribute.Name]);
|
||||||
|
|
||||||
|
if (propertyType.IsCollection)
|
||||||
{
|
{
|
||||||
ISettingValidator? validator = (ISettingValidator?)Activator.CreateInstance(settingAttribute.Validator, [settingAttribute.Name]);
|
TypeInfo validatedTypeInfo = UnwrapCollection(validatedTypes[0]);
|
||||||
if (validator != null)
|
if (validatedTypeInfo.IsCollection)
|
||||||
{
|
{
|
||||||
ValidationResult valres = await validator.ValidateAsync(convertedValue, cancellationToken).ConfigureAwait(false);
|
// validator validates collection => send whole collection as argument to ValidateAsync(..)
|
||||||
|
ValidationResult valres = await validatorInst.ValidateAsync(setPropertyResult.Value, cancellationToken).ConfigureAwait(false);
|
||||||
settings.ValidationResults.Add(valres);
|
settings.ValidationResults.Add(valres);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// validator validates elements => send items one by one to ValidateAsync(..)
|
||||||
|
if (setPropertyResult.Value is IList list)
|
||||||
|
{
|
||||||
|
foreach (object value in list)
|
||||||
|
{
|
||||||
|
ValidationResult valres = await validatorInst.ValidateAsync(value, cancellationToken).ConfigureAwait(false);
|
||||||
|
settings.ValidationResults.Add(valres);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// setting is not a collection. lets assume the validator doesn't validate collection as well
|
||||||
|
ValidationResult valres = await validatorInst.ValidateAsync(setPropertyResult.Value, cancellationToken).ConfigureAwait(false);
|
||||||
|
settings.ValidationResults.Add(valres);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
CLog.Error(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
CLog.Error(e, "Failed to validate property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
||||||
throw new CertMgrException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
throw new CertMgrException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (settingAttribute.IsMandatory)
|
|
||||||
{
|
|
||||||
ValidationResult valres = new ValidationResult(settingAttribute.Name, false, "Mandatory argument is missing");
|
|
||||||
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().ToString(false) ?? "<null>", elementType.ToString(false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await settings.ValidateAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,13 +182,6 @@ internal sealed class SettingsBuilder
|
|||||||
return rawArg != null;
|
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()
|
private IEnumerable<(PropertyInfo, SettingAttribute)> GetPropertiesWithSettingAttribute()
|
||||||
{
|
{
|
||||||
foreach (PropertyInfo propertyInfo in _settingsType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
foreach (PropertyInfo propertyInfo in _settingsType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||||
@@ -150,62 +220,65 @@ internal sealed class SettingsBuilder
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(bool success, object? convertedValue)> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type collectionType, Type elementType, CancellationToken cancellationToken)
|
private async Task<AsyncResult<object?>> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type collectionType, Type elementType, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
bool success = false;
|
bool success = false;
|
||||||
object? convertedValue = null;
|
object? convertedValue = null;
|
||||||
|
|
||||||
|
IValueConverter? customConverter = GetCustomConverter(settingAttribute);
|
||||||
|
|
||||||
if (isCollection)
|
if (isCollection)
|
||||||
{
|
{
|
||||||
if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter))
|
Type listType = typeof(List<>).MakeGenericType(elementType);
|
||||||
{
|
IList values = (IList)Activator.CreateInstance(listType)!;
|
||||||
Type listType = typeof(List<>).MakeGenericType(elementType);
|
|
||||||
IList values = (IList)Activator.CreateInstance(listType)!;
|
|
||||||
|
|
||||||
foreach (string rawValue in rawArg.Values)
|
foreach (string rawValue in rawArg.Values)
|
||||||
{
|
|
||||||
convertedValue = await customConverter.ConvertAsync(rawValue, elementType, cancellationToken).ConfigureAwait(false);
|
|
||||||
values.Add(convertedValue);
|
|
||||||
}
|
|
||||||
convertedValue = values;
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
Type listType = typeof(List<>).MakeGenericType(elementType);
|
AsyncResult<object?> converted = await ConvertValueAsync(rawValue, customConverter, elementType, cancellationToken).ConfigureAwait(false);
|
||||||
IList values = (IList)Activator.CreateInstance(listType)!;
|
if (converted.IsSuccess)
|
||||||
|
|
||||||
foreach (string rawValue in rawArg.Values)
|
|
||||||
{
|
{
|
||||||
if (TryConvertValue(rawValue, elementType, out convertedValue))
|
values.Add(converted.Value);
|
||||||
{
|
|
||||||
values.Add(convertedValue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
convertedValue = values; // BuildCollectionValue(collectionType, elementType, values);
|
|
||||||
success = true;
|
|
||||||
}
|
}
|
||||||
|
convertedValue = values;
|
||||||
|
success = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter))
|
AsyncResult<object?> converted = await ConvertValueAsync(rawArg.Values.First(), customConverter, elementType, cancellationToken).ConfigureAwait(false);
|
||||||
{
|
if (converted.IsSuccess)
|
||||||
convertedValue = await customConverter.ConvertAsync(rawArg.Values.First(), elementType, cancellationToken).ConfigureAwait(false);
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
else if (TryConvertValue(rawArg.Values.First(), elementType, out convertedValue))
|
|
||||||
{
|
{
|
||||||
|
convertedValue = converted.Value;
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
CLog.Error("Cannot convert value '{0}' of argument '{1}' to type '{2}'", rawArg.Values.First(), settingAttribute.Name, elementType.ToString(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (success, convertedValue);
|
return new AsyncResult<object?>(success, convertedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object BuildCollectionValue(Type collectionType, Type elementType, IReadOnlyList<object?> items)
|
private async Task<AsyncResult<object?>> ConvertValueAsync(string rawValue, IValueConverter? customConverter, Type elementType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
bool success;
|
||||||
|
object? convertedValue;
|
||||||
|
if (customConverter != null)
|
||||||
|
{
|
||||||
|
convertedValue = await customConverter.ConvertAsync(rawValue, elementType, cancellationToken).ConfigureAwait(false);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
success = TryConvertValue(rawValue, elementType, out convertedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AsyncResult<object?>(success, convertedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*private static object BuildCollectionValue(Type collectionType, Type elementType, IReadOnlyList<object?> items)
|
||||||
{
|
{
|
||||||
// convert source collection with 'items' of type 'object?' to collection with items of requested type:
|
// convert source collection with 'items' of type 'object?' to collection with items of requested type:
|
||||||
Type listType = typeof(List<>).MakeGenericType(elementType);
|
Type listType = typeof(List<>).MakeGenericType(elementType);
|
||||||
@@ -255,11 +328,11 @@ internal sealed class SettingsBuilder
|
|||||||
Array fallback = Array.CreateInstance(elementType, typedList.Count);
|
Array fallback = Array.CreateInstance(elementType, typedList.Count);
|
||||||
typedList.CopyTo(fallback, 0);
|
typedList.CopyTo(fallback, 0);
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
private bool TryGetCustomConverter(SettingAttribute settingAttribute, [NotNullWhen(true)] out IValueConverter? customConverter)
|
private IValueConverter? GetCustomConverter(SettingAttribute settingAttribute)
|
||||||
{
|
{
|
||||||
customConverter = null;
|
IValueConverter? customConverter = null;
|
||||||
|
|
||||||
Type? valueConverter = settingAttribute.Converter;
|
Type? valueConverter = settingAttribute.Converter;
|
||||||
if (valueConverter != null)
|
if (valueConverter != null)
|
||||||
@@ -274,16 +347,22 @@ internal sealed class SettingsBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return customConverter != null;
|
return customConverter;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryConvertValue(string rawValue, Type targetType, out object? convertedValue)
|
private bool TryConvertValue(string rawValue, Type targetType, out object? convertedValue)
|
||||||
{
|
{
|
||||||
convertedValue = null;
|
convertedValue = null;
|
||||||
|
|
||||||
|
Type? underlying = Nullable.GetUnderlyingType(targetType);
|
||||||
|
if (underlying != null)
|
||||||
|
{
|
||||||
|
targetType = underlying;
|
||||||
|
}
|
||||||
|
|
||||||
if (targetType.IsEnum)
|
if (targetType.IsEnum)
|
||||||
{
|
{
|
||||||
if (Enum.TryParse(targetType, rawValue, true, out object? result))
|
if (Enum.TryParse(targetType, rawValue, true, out object? result) && Enum.IsDefined(targetType, result))
|
||||||
{
|
{
|
||||||
convertedValue = result;
|
convertedValue = result;
|
||||||
}
|
}
|
||||||
@@ -308,37 +387,51 @@ internal sealed class SettingsBuilder
|
|||||||
return convertedValue != null;
|
return convertedValue != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (bool isCollection, Type? elementType) UnwrapCollection(Type type)
|
private static TypeInfo UnwrapCollection(Type type)
|
||||||
{
|
{
|
||||||
if (type == typeof(string))
|
if (type == typeof(string))
|
||||||
{
|
{
|
||||||
return (false, null);
|
return new TypeInfo(false, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Type testedType = type;
|
||||||
Type? underlying = Nullable.GetUnderlyingType(type);
|
Type? underlying = Nullable.GetUnderlyingType(type);
|
||||||
if (underlying != null)
|
if (underlying != null)
|
||||||
{
|
{
|
||||||
type = underlying;
|
testedType = underlying;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.IsArray)
|
if (testedType.IsArray)
|
||||||
{
|
{
|
||||||
return (true, type.GetElementType()!);
|
return new TypeInfo(true, testedType.GetElementType()!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
if (testedType.IsGenericType && testedType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||||
{
|
{
|
||||||
return (true, type.GetGenericArguments()[0]);
|
return new TypeInfo(true, testedType.GetGenericArguments()[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (Type i in type.GetInterfaces())
|
foreach (Type i in testedType.GetInterfaces())
|
||||||
{
|
{
|
||||||
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||||
{
|
{
|
||||||
return (true, i.GetGenericArguments()[0]);
|
return new TypeInfo(true, i.GetGenericArguments()[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (false, null);
|
return new TypeInfo(false, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TypeInfo
|
||||||
|
{
|
||||||
|
public TypeInfo(bool isCollection, Type elementType)
|
||||||
|
{
|
||||||
|
IsCollection = isCollection;
|
||||||
|
ElementType = elementType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCollection { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public Type ElementType { [DebuggerStepThrough] get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
certmgr/Core/Utils/AsyncResult.cs
Normal file
21
certmgr/Core/Utils/AsyncResult.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
public struct AsyncResult<T>
|
||||||
|
{
|
||||||
|
public AsyncResult(bool success, T value)
|
||||||
|
{
|
||||||
|
IsSuccess = success;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSuccess { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public T Value { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("{0}: {1}", IsSuccess ? "Succeeded" : "Failed", Value?.ToString() ?? "<null>");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.NetworkInformation;
|
using System.Net.NetworkInformation;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
using CertMgr.Core.Exceptions;
|
using CertMgr.Core.Exceptions;
|
||||||
|
|
||||||
@@ -7,6 +9,87 @@ namespace CertMgr.Core.Utils;
|
|||||||
|
|
||||||
public static class NetUtils
|
public static class NetUtils
|
||||||
{
|
{
|
||||||
|
// following regex validates string as DNS names:
|
||||||
|
// - every label has up to 63 chars
|
||||||
|
// - label contains only [a-z][A-Z][0-9]-
|
||||||
|
// - label cannot start and/or end with dash char ('-')
|
||||||
|
// - full name has up to 253 chars
|
||||||
|
// - case insensitive
|
||||||
|
// private static readonly Regex DNSRegex = new Regex(@"^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?)(?:\.[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?)*$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
// following regex accepts (in addition to previous one):
|
||||||
|
// - wildcard certificates (e.g. *.example.com) and
|
||||||
|
// - 'root label' (terminating dot) (e.g. example.com.)
|
||||||
|
private static readonly Regex DNSRegex = new Regex(@"^(?=.{1,254}$)(?:\*\.)?(?:[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?)(?:\.[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?)*\.?$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Following code is not used as IPAddress.TryParse(...) seems to be simpler choice to verify input string, but keeping both ipv4 and ipv6 regex just in case
|
||||||
|
|
||||||
|
private const string IPv4octet = "(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])";
|
||||||
|
private const string IPv6hextet = "[0-9A-F]"; // should be [0-9A-Fa-f] but since Regex is created with RegexOptions.IgnoreCase it is not necessary
|
||||||
|
private const string IPv4 = "(?:" + IPv4octet + @"\.){3}" + IPv4octet;
|
||||||
|
|
||||||
|
private static readonly Regex IPv4Regex = new Regex("^" + IPv4 + "$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
private static readonly Regex IPv6Regex = new Regex(
|
||||||
|
@"^(?:(?:" + IPv6hextet + @"{1,4}:){7}" + IPv6hextet + @"{1,4}|(?:" + IPv6hextet + @"{1,4}:){1,7}:" +
|
||||||
|
@"|" +
|
||||||
|
@"(?:" + IPv6hextet + @"{1,4}:){1,6}:" + IPv6hextet + @"{1,4}" +
|
||||||
|
@"|" +
|
||||||
|
@"(?:" + IPv6hextet + @"{1,4}:){1,5}(?::" + IPv6hextet + @"{1,4}){1,2}" +
|
||||||
|
@"|" +
|
||||||
|
@"(?:" + IPv6hextet + @"{1,4}:){1,4}(?::" + IPv6hextet + @"{1,4}){1,3}" +
|
||||||
|
@"|" +
|
||||||
|
@"(?:" + IPv6hextet + @"{1,4}:){1,3}(?::" + IPv6hextet + @"{1,4}){1,4}" +
|
||||||
|
@"|" +
|
||||||
|
@"(?:" + IPv6hextet + @"{1,4}:){1,2}(?::" + IPv6hextet + @"{1,4}){1,5}" +
|
||||||
|
@"|" +
|
||||||
|
@"" + IPv6hextet + @"{1,4}:(?:(?::" + IPv6hextet + @"{1,4}){1,6})" +
|
||||||
|
@"|" +
|
||||||
|
@":(?:(?::" + IPv6hextet + @"{1,4}){1,7}|:)" +
|
||||||
|
@"|" +
|
||||||
|
@"(?:" + IPv6hextet + @"{1,4}:){6}" + IPv4 +
|
||||||
|
@"|" +
|
||||||
|
@"(?:" + IPv6hextet + @"{1,4}:){1,5}:" + IPv4 +
|
||||||
|
@"|" +
|
||||||
|
@"(?:" + IPv6hextet + @"{1,4}:){1,4}:(?::" + IPv6hextet + @"{1,4})?" +
|
||||||
|
@"(?::" + IPv6hextet + @"{1,4})?" + @":" + IPv4 +
|
||||||
|
@"|" +
|
||||||
|
@"(?:" + IPv6hextet + @"{1,4}:){1,3}(?::" + IPv6hextet + @"{1,4}){1,2}:" + IPv4 +
|
||||||
|
@"|" +
|
||||||
|
@"(?:" + IPv6hextet + @"{1,4}:){1,2}(?::" + IPv6hextet + @"{1,4}){1,3}:" + IPv4 +
|
||||||
|
@"|" +
|
||||||
|
@"" + IPv6hextet + @"{1,4}(?::" + IPv6hextet + @"{1,4}){1,4}:" + IPv4 +
|
||||||
|
@"|" +
|
||||||
|
@"::(?:[0-9A-Fa-f]{1,4}:){1,5}" + IPv4 +
|
||||||
|
@"|" +
|
||||||
|
@"::" + IPv4 +
|
||||||
|
@")$",
|
||||||
|
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public static bool IsValidIPv4(ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
if (value.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool match = IPv4Regex.IsMatch(value);
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidIPv6(ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
if (value.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool match = IPv6Regex.IsMatch(value);
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/// <summary>Returns DNS name of the local computer.</summary>
|
/// <summary>Returns DNS name of the local computer.</summary>
|
||||||
public static string MachineName => GetMachineName(MachineNameFormat.Hostname);
|
public static string MachineName => GetMachineName(MachineNameFormat.Hostname);
|
||||||
|
|
||||||
@@ -34,4 +117,260 @@ public static class NetUtils
|
|||||||
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsValidDns(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsValidDns(value.AsSpan());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidDns(ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
if (value.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool match = DNSRegex.IsMatch(value);
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidIPv4(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsValidIPv4(value.AsSpan());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidIPv4(ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
bool valid = false;
|
||||||
|
|
||||||
|
if (IPAddress.TryParse(value, out IPAddress? ip))
|
||||||
|
{
|
||||||
|
valid = ip.AddressFamily == AddressFamily.InterNetwork;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidIPv6(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsValidIPv6(value.AsSpan());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidIPv6(ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
bool valid = false;
|
||||||
|
|
||||||
|
// it seems that IPAddress.TryParse doesn't handle zone/scope-id correctly (at least on windows)
|
||||||
|
// %eth0 or %eth1 and basically all non-numeric strings are "translated" to scope-id = 0
|
||||||
|
// (possibly because such a zones/scope-ids doesn't exist on my machine?)
|
||||||
|
// on the other hand - when ip ends with %5 then the scope-id is kept (even if such a zone/scope-id doesn't exist on my machine
|
||||||
|
if (IPAddress.TryParse(value, out IPAddress? ip))
|
||||||
|
{
|
||||||
|
valid = ip.AddressFamily == AddressFamily.InterNetworkV6;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidIPAny(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsValidIPAny(value.AsSpan());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidIPAny(ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
if (value.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool match = IsValidIPv4(value) || IsValidIPv6(value);
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsSameMachine(string machineA, string machineB)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(machineA) || string.IsNullOrWhiteSpace(machineB))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
machineA = machineA.Trim();
|
||||||
|
machineB = machineB.Trim();
|
||||||
|
|
||||||
|
string textualA = machineA.TrimEnd('.').ToLowerInvariant();
|
||||||
|
string textualB = machineB.TrimEnd('.').ToLowerInvariant();
|
||||||
|
if (textualA == textualB)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IPAddress[] addrSetA = Dns.GetHostAddresses(machineA);
|
||||||
|
IPAddress[] addrSetB = Dns.GetHostAddresses(machineB);
|
||||||
|
|
||||||
|
if (addrSetA.Length == 0 || addrSetB.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<IPAddress> normalizedAddrSetA = new HashSet<IPAddress>(addrSetA.Select(NormalizeIp));
|
||||||
|
foreach (IPAddress addrB in addrSetB)
|
||||||
|
{
|
||||||
|
IPAddress normalizedAddrB = NormalizeIp(addrB);
|
||||||
|
if (normalizedAddrSetA.Contains(normalizedAddrB))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (SocketException e)
|
||||||
|
{
|
||||||
|
e.GetType();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (ArgumentException e)
|
||||||
|
{
|
||||||
|
e.GetType();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsLocalMachine(string machine)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(machine))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string normalizedMachine = machine.Trim().TrimEnd('.').ToLowerInvariant();
|
||||||
|
|
||||||
|
if (normalizedMachine == "localhost" || normalizedMachine == "127.0.0.1" || normalizedMachine == "::1" || normalizedMachine == MachineName.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<IPAddress> localAddrSet = GetNormalizedLocalIpSet(true);
|
||||||
|
|
||||||
|
// possibly it is IP - this can be solved without involving the DNS:
|
||||||
|
if (IPAddress.TryParse(normalizedMachine, out IPAddress? machineIp))
|
||||||
|
{
|
||||||
|
if (IPAddress.IsLoopback(machineIp))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IPAddress normalizedMachineIp = NormalizeIp(machineIp);
|
||||||
|
return localAddrSet.Contains(normalizedMachineIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// not IP, DNS needed:
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IPAddress[] resolved = Dns.GetHostAddresses(normalizedMachine);
|
||||||
|
if (resolved.Length > 0)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < resolved.Length; i++)
|
||||||
|
{
|
||||||
|
IPAddress normalized = NormalizeIp(resolved[i]);
|
||||||
|
if (localAddrSet.Contains(normalized))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
e.GetType();
|
||||||
|
// DNS failed
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<IPAddress> GetNormalizedLocalIpSet(bool includeLoopbacks)
|
||||||
|
{
|
||||||
|
HashSet<IPAddress> result = new HashSet<IPAddress>();
|
||||||
|
|
||||||
|
foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces())
|
||||||
|
{
|
||||||
|
if (ni.OperationalStatus != OperationalStatus.Up)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
IPInterfaceProperties? props;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
props = ni.GetIPProperties();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (UnicastIPAddressInformation ua in props.UnicastAddresses)
|
||||||
|
{
|
||||||
|
IPAddress address = ua.Address;
|
||||||
|
|
||||||
|
if (!includeLoopbacks && IPAddress.IsLoopback(address))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(NormalizeIp(address));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeLoopbacks)
|
||||||
|
{
|
||||||
|
result.Add(NormalizeIp(IPAddress.Loopback));
|
||||||
|
result.Add(NormalizeIp(IPAddress.IPv6Loopback));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IPAddress NormalizeIp(IPAddress ip)
|
||||||
|
{
|
||||||
|
// convert 'IPv6-mapped IPv4' to IPv4 (e.g. (::ffff:a.b.c.d => a.b.c.d):
|
||||||
|
if (ip.AddressFamily == AddressFamily.InterNetworkV6 && ip.IsIPv4MappedToIPv6)
|
||||||
|
{
|
||||||
|
return ip.MapToIPv4();
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove scope-id (e.g. fe80::1%eth0 → fe80::1):
|
||||||
|
if (ip.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
|
{
|
||||||
|
byte[] bytes = ip.GetAddressBytes();
|
||||||
|
IPAddress withoutScope = new IPAddress(bytes);
|
||||||
|
return withoutScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
certmgr/Core/Validation/SettingValidatorT.cs
Normal file
32
certmgr/Core/Validation/SettingValidatorT.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
public abstract class SettingValidator<T> : ISettingValidator<T>
|
||||||
|
{
|
||||||
|
protected SettingValidator(string settingName)
|
||||||
|
{
|
||||||
|
SettingName = settingName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SettingName { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public abstract Task<ValidationResult> ValidateAsync(T? settingValue, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
public Task<ValidationResult> ValidateAsync(object? settingValue, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
T? typedValue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
typedValue = (T?)settingValue;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new CertMgrException(e, "SettingValidator of type '{0}' failed to convert value of type '{1}' to type '{2}' (setting-name = '{3}')", GetType().ToString(false), settingValue?.GetType().ToString(false) ?? "<null>", typeof(T).ToString(false), SettingName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidateAsync(typedValue, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
using CertMgr.CertGen;
|
using CertMgr.CertGen;
|
||||||
|
using CertMgr.CertGen.Utils;
|
||||||
using CertMgr.Core.Attributes;
|
using CertMgr.Core.Attributes;
|
||||||
using CertMgr.Core.Converters.Impl;
|
using CertMgr.Core.Converters.Impl;
|
||||||
using CertMgr.Core.Jobs;
|
using CertMgr.Core.Jobs;
|
||||||
@@ -19,10 +20,15 @@ public sealed class CertificateSettings : JobSettings
|
|||||||
ValidityPeriod = TimeSpan.FromDays(365);
|
ValidityPeriod = TimeSpan.FromDays(365);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Setting("subject", IsMandatory = true, Validator = typeof(StringValidator.IsNotNull))]
|
[Setting("subject", IsMandatory = true, Validator = typeof(SubjectValidator))]
|
||||||
public string? Subject { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
public string? Subject { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
[Setting("subject-alternate-name", AlternateNames = ["san"])]
|
// value can be:
|
||||||
|
// - plain hostname
|
||||||
|
// - DNS:hostname (i.e. with 'DNS:' prefix)
|
||||||
|
// - IP:ip-address (i.e. with 'IP:' prefix)
|
||||||
|
// other types (URI, UPN, email) are not supported
|
||||||
|
[Setting("subject-alternate-name", AlternateNames = ["san"], Validator = typeof(SubjectAlternateNameValidator))]
|
||||||
public IReadOnlyCollection<string>? SubjectAlternateNames { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
public IReadOnlyCollection<string>? SubjectAlternateNames { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
[Setting("algorithm", Default = CertificateAlgorithm.ECDsa, Converter = typeof(EnumConverter))]
|
[Setting("algorithm", Default = CertificateAlgorithm.ECDsa, Converter = typeof(EnumConverter))]
|
||||||
@@ -31,7 +37,7 @@ public sealed class CertificateSettings : JobSettings
|
|||||||
[Setting("ecdsa-curve")]
|
[Setting("ecdsa-curve")]
|
||||||
public EcdsaCurve? Curve { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
public EcdsaCurve? Curve { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
[Setting("rsa-key-size")]
|
[Setting("rsa-key-size", Converter = typeof(RsaKeySizeConverter))]
|
||||||
public RsaKeySize? RsaKeySize { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
public RsaKeySize? RsaKeySize { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
[Setting("hash-algorithm", AlternateNames = ["ha"])]
|
[Setting("hash-algorithm", AlternateNames = ["ha"])]
|
||||||
@@ -70,7 +76,7 @@ public sealed class CertificateSettings : JobSettings
|
|||||||
}
|
}
|
||||||
else if (Algorithm == CertificateAlgorithm.RSA)
|
else if (Algorithm == CertificateAlgorithm.RSA)
|
||||||
{
|
{
|
||||||
if (!RsaKeySize.HasValue || !Enum.IsDefined(RsaKeySize.Value))
|
if (!RsaKeySize.HasValue || !Enum.IsDefined<RsaKeySize>(RsaKeySize.Value))
|
||||||
{
|
{
|
||||||
results.AddInvalid(nameof(RsaKeySize), "value value must be specified: '{0}'", RsaKeySize?.ToString() ?? "<null>");
|
results.AddInvalid(nameof(RsaKeySize), "value value must be specified: '{0}'", RsaKeySize?.ToString() ?? "<null>");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,10 +75,17 @@ public sealed class CreateCertificateJob : Job<CertificateSettings>
|
|||||||
cgcs.FriendlyName = "I'm your friend";
|
cgcs.FriendlyName = "I'm your friend";
|
||||||
if (Settings.Issuer != null)
|
if (Settings.Issuer != null)
|
||||||
{
|
{
|
||||||
using (MemoryStream ms = new MemoryStream())
|
try
|
||||||
{
|
{
|
||||||
await Settings.Issuer.ReadAsync(ms, cancellationToken).ConfigureAwait(false);
|
using (MemoryStream ms = new MemoryStream())
|
||||||
cgcs.Issuer = X509CertificateLoader.LoadPkcs12(ms.GetBuffer(), Settings.IssuerPassword, flags);
|
{
|
||||||
|
await Settings.Issuer.ReadAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||||
|
cgcs.Issuer = X509CertificateLoader.LoadPkcs12(ms.GetBuffer(), Settings.IssuerPassword, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new CertGenException(e, "Failed to load issuer's certificate");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cgcs.SubjectName = Settings.Subject;
|
cgcs.SubjectName = Settings.Subject;
|
||||||
|
|||||||
49
certmgr/Jobs/RsaKeySizeConverter.cs
Normal file
49
certmgr/Jobs/RsaKeySizeConverter.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using CertMgr.CertGen;
|
||||||
|
using CertMgr.Core.Converters;
|
||||||
|
|
||||||
|
namespace CertMgr.Jobs;
|
||||||
|
|
||||||
|
internal sealed class RsaKeySizeConverter : ValueConverter<RsaKeySize?>
|
||||||
|
{
|
||||||
|
protected override Task<RsaKeySize?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(rawValue))
|
||||||
|
{
|
||||||
|
return Task.FromResult((RsaKeySize?)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
RsaKeySize? keySize;
|
||||||
|
|
||||||
|
Type? realType = Nullable.GetUnderlyingType(targetType);
|
||||||
|
if (realType == null)
|
||||||
|
{
|
||||||
|
realType = targetType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Enum.TryParse(rawValue, true, out RsaKeySize tmp) && Enum.IsDefined(realType, tmp))
|
||||||
|
{
|
||||||
|
keySize = tmp;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (string.Equals(rawValue, "2048"))
|
||||||
|
{
|
||||||
|
keySize = RsaKeySize.KeySize2048;
|
||||||
|
}
|
||||||
|
else if (string.Equals(rawValue, "4096"))
|
||||||
|
{
|
||||||
|
keySize = RsaKeySize.KeySize4096;
|
||||||
|
}
|
||||||
|
else if (string.Equals(rawValue, "8192"))
|
||||||
|
{
|
||||||
|
keySize = RsaKeySize.KeySize8192;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
keySize = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(keySize);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
certmgr/Jobs/SubjectAlternateNameValidator.cs
Normal file
46
certmgr/Jobs/SubjectAlternateNameValidator.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using CertMgr.Core.Utils;
|
||||||
|
using CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
namespace CertMgr.Jobs;
|
||||||
|
|
||||||
|
internal sealed class SubjectAlternateNameValidator : SettingValidator<string>
|
||||||
|
{
|
||||||
|
public SubjectAlternateNameValidator(string settingName)
|
||||||
|
: base(settingName)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<ValidationResult> ValidateAsync(string? settingValue, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(settingValue))
|
||||||
|
{
|
||||||
|
return Task.FromResult(new ValidationResult(SettingName, false, "value must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadOnlySpan<char> span = settingValue.AsSpan();
|
||||||
|
if (span.StartsWith("DNS:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (!NetUtils.IsValidDns(span.Slice(4)))
|
||||||
|
{
|
||||||
|
return Task.FromResult(new ValidationResult(SettingName, false, "value '{0}' is not valid DNS name", span.Slice(4).ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (span.StartsWith("IP:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (!NetUtils.IsValidIPAny(span.Slice(3)))
|
||||||
|
{
|
||||||
|
return Task.FromResult(new ValidationResult(SettingName, false, "value '{0}' is not valid IP address", span.Slice(3).ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// fallback to dns name as other alt-names (UPN, email, URI..) are not supported
|
||||||
|
if (!NetUtils.IsValidDns(span))
|
||||||
|
{
|
||||||
|
return Task.FromResult(new ValidationResult(SettingName, false, "value '{0}' is not valid DNS name (no prefix)", span.Slice(4).ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(new ValidationResult(SettingName, true, "valid"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,18 +6,31 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
private static async Task<int> Main(string[] args)
|
private static async Task<int> Main(string[] args)
|
||||||
{
|
{
|
||||||
|
// args = [
|
||||||
|
// "--job=create-certificate",
|
||||||
|
// "--issuer-certificate=file|o|c:\\friend2.pfx",
|
||||||
|
// "--issuer-password=aaa",
|
||||||
|
// "--subject=hello",
|
||||||
|
// "--san=world",
|
||||||
|
// "--algorithm=ecdsa",
|
||||||
|
// "--ecdsa-curve=p384",
|
||||||
|
// "--storage=file|w|c:\\mycert.pfx",
|
||||||
|
// "--validity-period=2d" ];
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
"--job=create-certificate",
|
"--job=create-certificate",
|
||||||
"--issuer-certificate=file|o|c:\\friend2.pfx",
|
"--issuer-certificate=file|o|c:\\friend2.pfx",
|
||||||
"--issuer-password=aaa",
|
"--issuer-password=aaa",
|
||||||
"--subject=hello",
|
"--subject=CN=hello",
|
||||||
"--san=world",
|
"--san=world",
|
||||||
"--algorithm=ecdsa",
|
"--san=DNS:zdrastvujte",
|
||||||
"--ecdsa-curve=p384",
|
"--san=IP:192.168.131.1",
|
||||||
"--storage=file|w|c:\\mycert.pfx",
|
"--algorithm=rsa",
|
||||||
|
"--rsa-key-size=2048",
|
||||||
|
"--storage=file|w|c:\\friend-rsa.pfx",
|
||||||
"--validity-period=2d" ];
|
"--validity-period=2d" ];
|
||||||
|
|
||||||
using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
|
using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
|
||||||
|
|
||||||
JobExecutor executor = new JobExecutor();
|
JobExecutor executor = new JobExecutor();
|
||||||
int errorLevel = await executor.ExecuteAsync(args, cts.Token).ConfigureAwait(false);
|
int errorLevel = await executor.ExecuteAsync(args, cts.Token).ConfigureAwait(false);
|
||||||
|
|||||||
86
certmgrTest/NetUtilsTest.cs
Normal file
86
certmgrTest/NetUtilsTest.cs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace certmgrTest
|
||||||
|
{
|
||||||
|
public class NetUtilsTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void IPv6Test()
|
||||||
|
{
|
||||||
|
string[] validIPv6Addresses = new[]
|
||||||
|
{
|
||||||
|
"2001:0db8:0000:0000:0000:ff00:0042:8329", // full form
|
||||||
|
"2001:db8:0:0:0:ff00:42:8329", // shortened zeros
|
||||||
|
"2001:db8::ff00:42:8329", // compressed zeros
|
||||||
|
"::1", // loopback
|
||||||
|
"::", // unspecified address
|
||||||
|
"fe80::1ff:fe23:4567:890a", // link-local address
|
||||||
|
"2001:db8:1234:ffff:ffff:ffff:ffff:ffff", // maximum range
|
||||||
|
"fd12:3456:789a:1::1", // unique local address (ULA)
|
||||||
|
"2001:0:3238:DFE1:63::FEFB", // mixed compression
|
||||||
|
"0:0:0:0:0:0:0:1", // loopback without compression
|
||||||
|
"2001:db8::123", // short address
|
||||||
|
"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", // all bits set to 1
|
||||||
|
"2001:db8:85a3::8a2e:370:7334", // example from RFC 4291
|
||||||
|
"::ffff:192.0.2.128" // IPv4-mapped IPv6 address
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
foreach (string ip in validIPv6Addresses)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool valid = NetUtils.IsValidIPv6(ip);
|
||||||
|
Assert.That(valid, Is.True, string.Format("expected that IPv6 address '{0}' is valid", ip));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Assert.Fail(string.Format("address: '{0}' failed with: {1}: {2}", ip, e.GetType().Name, e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IPv6_InvalidTest()
|
||||||
|
{
|
||||||
|
string[] invalidIPv6Addresses = new[]
|
||||||
|
{
|
||||||
|
"2001:::85a3::8a2e:370:7334", // multiple "::"
|
||||||
|
"2001:db8:85a3:0:0:8a2e:370g:7334", // invalid hex character (g)
|
||||||
|
"1200::AB00:1234::2552:7777:1313", // two "::" sections
|
||||||
|
"2001:db8:85a3:z:0:8a2e:370:7334", // invalid hex segment
|
||||||
|
"2001:db8:85a3:0:0:8a2e:370:7334:1234", // too many segments (9)
|
||||||
|
"2001:db8:85a3", // too few segments (3)
|
||||||
|
":2001:db8::1", // leading colon without pair
|
||||||
|
"2001:db8::1:", // trailing colon without pair
|
||||||
|
"2001:db8::1::", // multiple compression points
|
||||||
|
"2001:db8:85a3:0:0:8a2e:370:7334/64", // contains CIDR mask
|
||||||
|
// "2001:db8:85a3:0:0:8a2e:370:7334%", // missing interface ID after % <= this is valid (at least on windows)
|
||||||
|
"::ffff:999.0.2.128", // invalid IPv4-mapped part
|
||||||
|
"::ffff:192.0.2.256", // IPv4 part out of range
|
||||||
|
"2001:db8:85a3::8a2e:370:7334:xyz", // trailing junk
|
||||||
|
"2001:db8:::", // triple colon
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
foreach (string ip in invalidIPv6Addresses)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool valid = NetUtils.IsValidIPv6(ip);
|
||||||
|
Assert.That(valid, Is.False, string.Format("expected that IPv6 address '{0}' is NOT valid", ip));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Assert.Fail(string.Format("address: '{0}' failed with: {1}: {2}", ip, e.GetType().Name, e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user