filtering (cert-store)

This commit is contained in:
2025-11-04 11:17:27 +01:00
parent fbe64afd41
commit f382b7c47c
19 changed files with 288 additions and 57 deletions

View File

@@ -0,0 +1,11 @@
using CertMgr.Core.Filters;
namespace CertMgr.Certificates.Filters;
internal sealed class ByIssuerFilter : CertificateFilter
{
public ByIssuerFilter(string pattern, FilteringFlags flags)
: base(pattern, flags, cert => cert.Issuer)
{
}
}

View File

@@ -0,0 +1,11 @@
using CertMgr.Core.Filters;
namespace CertMgr.Certificates.Filters;
internal sealed class BySubjectFilter : CertificateFilter
{
public BySubjectFilter(string pattern, FilteringFlags flags)
: base(pattern, flags, cert => cert.Subject)
{
}
}

View File

@@ -1,21 +1,11 @@
using System.Diagnostics;
using System.Security.Cryptography.X509Certificates;
using CertMgr.Core.Filters;
namespace CertMgr.Certificates.Filters;
internal sealed class ByThumbprintFilter : CertificateFilter
{
internal ByThumbprintFilter(string thumbprint)
: base(CertificateFilterType.Thumbprint)
public ByThumbprintFilter(string pattern, FilteringFlags flags)
: base(pattern, flags, cert => cert.Thumbprint)
{
Thumbprint = thumbprint;
}
private string Thumbprint { [DebuggerStepThrough] get; }
protected override Task<bool> DoIsMatchAsync(X509Certificate2 value, CancellationToken cancellationToken)
{
bool result = string.Equals(value.Thumbprint, Thumbprint, StringComparison.OrdinalIgnoreCase);
return Task.FromResult(result);
}
}

View File

@@ -5,17 +5,51 @@ using CertMgr.Core.Filters;
namespace CertMgr.Certificates.Filters;
public abstract class CertificateFilter : Filter<X509Certificate2>
internal abstract class CertificateFilter : Filter<X509Certificate2>
{
internal CertificateFilter(CertificateFilterType type)
protected CertificateFilter(string pattern, FilteringFlags flags, Func<X509Certificate2, string> propertyGetter)
{
Type = type;
Pattern = pattern;
PropertyGetter = propertyGetter;
Worker = GetWorker(flags);
}
public CertificateFilterType Type { [DebuggerStepThrough] get; }
private string Pattern { [DebuggerStepThrough] get; }
public override string ToString()
private Func<X509Certificate2, string> PropertyGetter { [DebuggerStepThrough] get; }
private IFilter<string> Worker { [DebuggerStepThrough] get; }
protected sealed override async Task<bool> DoIsMatchAsync(X509Certificate2 value, CancellationToken cancellationToken)
{
return string.Format("type = {0}", Type);
string property = PropertyGetter(value);
bool result = await Worker.IsMatchAsync(property, cancellationToken).ConfigureAwait(false);
return result;
}
private IFilter<string> GetWorker(FilteringFlags flags)
{
IFilter<string> worker;
if (flags.HasFlag(FilteringFlags.Exact))
{
worker = Core.Filters.KnownFilters.String.ExactMatch(Pattern, flags.HasFlag(FilteringFlags.CaseSensitive));
}
else if (flags.HasFlag(FilteringFlags.Wildcard))
{
worker = Core.Filters.KnownFilters.String.Wildcard(Pattern, flags.HasFlag(FilteringFlags.CaseSensitive));
}
else if (flags.HasFlag(FilteringFlags.Regex))
{
worker = Core.Filters.KnownFilters.String.Regex(Pattern, flags.HasFlag(FilteringFlags.CaseSensitive));
}
else
{
// default search is 'Exact'
worker = Core.Filters.KnownFilters.String.ExactMatch(Pattern, flags.HasFlag(FilteringFlags.CaseSensitive));
}
return worker;
}
}

View File

@@ -2,5 +2,7 @@
public enum CertificateFilterType
{
Thumbprint = 1
Thumbprint = 1,
Subject,
Issuer
}

View File

@@ -8,9 +8,19 @@ public static class KnownFilters
{
public static class Certificate
{
public static IFilter<X509Certificate2> ByThumbprint(string thumbprint)
public static IFilter<X509Certificate2> ByThumbprint(string thumbprint, FilteringFlags flags)
{
return new ByThumbprintFilter(thumbprint);
return new ByThumbprintFilter(thumbprint, flags);
}
public static IFilter<X509Certificate2> BySubject(string subjectName, FilteringFlags flags)
{
return new BySubjectFilter(subjectName, flags);
}
public static IFilter<X509Certificate2> ByIssuer(string issuerName, FilteringFlags flags)
{
return new ByIssuerFilter(issuerName, flags);
}
}
}

View File

@@ -16,6 +16,11 @@ public abstract class Filter<T> : IFilter<T>
protected abstract Task<bool> DoIsMatchAsync(T value, CancellationToken cancellationToken);
public override string ToString()
{
return string.Format("type = {0}", GetType().ToString(false));
}
private sealed class EmptyFilter : Filter<T>
{
internal EmptyFilter(bool result)

View File

@@ -0,0 +1,13 @@
namespace CertMgr.Core.Filters;
[Flags]
public enum FilteringFlags
{
None = 0,
CaseSensitive = 1 << 1,
Exact = 1 << 3,
Wildcard = 1 << 4,
Regex = 1 << 5,
}

View File

@@ -0,0 +1,29 @@
using System.Diagnostics;
using CertMgr.Core.Utils;
namespace CertMgr.Core.Filters.Impl;
internal sealed class ExactMatchStringFilter : Filter<string>
{
internal ExactMatchStringFilter(string pattern, bool caseSensitive)
{
Pattern = pattern;
CaseSensitive = caseSensitive;
}
private string Pattern { [DebuggerStepThrough] get; }
private bool CaseSensitive { [DebuggerStepThrough] get; }
protected override Task<bool> DoIsMatchAsync(string value, CancellationToken cancellationToken)
{
bool ismatch = string.Equals(Pattern, value, CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
return Task.FromResult(ismatch);
}
public override string ToString()
{
return string.Format("type = {0}, pattern = '{1}', case-sensitive = {2}", GetType().ToString(false), Pattern, CaseSensitive ? "yes" : "no");
}
}

View File

@@ -0,0 +1,27 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
using CertMgr.Core.Utils;
namespace CertMgr.Core.Filters.Impl;
internal sealed class RegexStringFilter : Filter<string>
{
internal RegexStringFilter(string regexPattern, bool caseSensitive)
{
Regex = new Regex(regexPattern, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
}
private Regex Regex { [DebuggerStepThrough] get; }
protected override Task<bool> DoIsMatchAsync(string value, CancellationToken cancellationToken)
{
bool ismatch = Regex.IsMatch(value);
return Task.FromResult(ismatch);
}
public override string ToString()
{
return string.Format("type = {0}, pattern = '{1}', case-sensitive = {2}", GetType().ToString(false), Regex, Regex.Options.HasFlag(RegexOptions.IgnoreCase) ? "no" : "yes");
}
}

View File

@@ -0,0 +1,21 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace CertMgr.Core.Filters.Impl;
internal sealed class WildcardStringFilter : Filter<string>
{
internal WildcardStringFilter(string pattern, bool caseSensitive)
{
string regexPattern = "^" + Regex.Escape(pattern).Replace("\\?", ".").Replace("\\*", ".*") + "$";
Regex = new Regex(regexPattern, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
}
private Regex Regex { [DebuggerStepThrough] get; }
protected override Task<bool> DoIsMatchAsync(string value, CancellationToken cancellationToken)
{
bool ismatch = Regex.IsMatch(value);
return Task.FromResult(ismatch);
}
}

View File

@@ -0,0 +1,24 @@
using CertMgr.Core.Filters.Impl;
namespace CertMgr.Core.Filters;
public static class KnownFilters
{
public static class String
{
public static IFilter<string> ExactMatch(string pattern, bool caseSensitive)
{
return new ExactMatchStringFilter(pattern, caseSensitive);
}
public static IFilter<string> Wildcard(string pattern, bool caseSensitive)
{
return new WildcardStringFilter(pattern, caseSensitive);
}
public static IFilter<string> Regex(string pattern, bool caseSensitive)
{
return new RegexStringFilter(pattern, caseSensitive);
}
}
}

View File

@@ -0,0 +1,8 @@
namespace CertMgr.Core.Filters;
public enum StringFilterType
{
ExactMatch = 1,
Wildcard,
Regex
}

View File

@@ -26,12 +26,34 @@ internal sealed class CertificateFilterConverter : ValueConverter<IFilter<X509Ce
ReadOnlySpan<char> filterTypeSpan = span.Slice(0, separatorIndex);
if (Enum.TryParse(filterTypeSpan, true, out CertificateFilterType filterType) && Enum.IsDefined(filterType))
{
ReadOnlySpan<char> nameSpan = span.Slice(separatorIndex + 1, span.Length - separatorIndex - 1);
string value = nameSpan.ToString();
ReadOnlySpan<char> valueSpan = span.Slice(separatorIndex + 1, span.Length - separatorIndex - 1);
separatorIndex = valueSpan.IndexOf(':');
string pattern;
FilteringFlags flags;
if (separatorIndex == -1)
{
pattern = valueSpan.ToString();
flags = FilteringFlags.Exact;
}
else
{
ReadOnlySpan<char> flagsSpan = valueSpan.Slice(0, separatorIndex);
flags = GetFilteringFlags(flagsSpan);
pattern = valueSpan.Slice(separatorIndex + 1).ToString();
}
switch (filterType)
{
case CertificateFilterType.Thumbprint:
filter = KnownFilters.Certificate.ByThumbprint(value);
filter = Certificates.Filters.KnownFilters.Certificate.ByThumbprint(pattern, flags);
break;
case CertificateFilterType.Subject:
filter = Certificates.Filters.KnownFilters.Certificate.BySubject(pattern, flags);
break;
case CertificateFilterType.Issuer:
filter = Certificates.Filters.KnownFilters.Certificate.ByIssuer(pattern, flags);
break;
default:
break;
@@ -39,7 +61,7 @@ internal sealed class CertificateFilterConverter : ValueConverter<IFilter<X509Ce
}
else
{
CLog.Error("Failed to parse filter type from value '{0}'. (available values: {1})", rawValue, Enum.GetNames<CertificateFilterType>().ToSeparatedList());
CLog.Error("Failed to parse filter type from value '{0}'. (available values = {1})", rawValue, Enum.GetNames<CertificateFilterType>().ToSeparatedList());
}
}
else
@@ -49,4 +71,21 @@ internal sealed class CertificateFilterConverter : ValueConverter<IFilter<X509Ce
return Task.FromResult(filter);
}
private FilteringFlags GetFilteringFlags(ReadOnlySpan<char> flagsSpan)
{
FilteringFlags flags = FilteringFlags.None;
MemoryExtensions.SpanSplitEnumerator<char> enu = flagsSpan.Split(',');
while (enu.MoveNext())
{
ReadOnlySpan<char> currentFlag = flagsSpan[enu.Current].Trim();
if (Enum.TryParse(currentFlag, true, out FilteringFlags tmp) && Enum.IsDefined(tmp))
{
flags |= tmp;
}
}
return flags;
}
}

View File

@@ -14,7 +14,7 @@ public sealed class GetCertificateInfoJob : Job<GetCertificateInfoSettings>
protected override async Task<JobResult> DoExecuteAsync(CancellationToken cancellationToken)
{
X509Certificate2? cert = null;
List<X509Certificate2> certs = new List<X509Certificate2>();
if (!string.IsNullOrEmpty(Settings.File))
{
@@ -22,7 +22,8 @@ public sealed class GetCertificateInfoJob : Job<GetCertificateInfoSettings>
{
try
{
cert = X509CertificateLoader.LoadPkcs12FromFile(Settings.File, Settings.Password);
X509Certificate2 cert = X509CertificateLoader.LoadPkcs12FromFile(Settings.File, Settings.Password);
certs.Add(cert);
}
catch (Exception e)
{
@@ -39,8 +40,8 @@ public sealed class GetCertificateInfoJob : Job<GetCertificateInfoSettings>
if (Settings.Filter != null)
{
CertStoreSearcher searcher = new CertStoreSearcher(Settings.Store);
IReadOnlyList<X509Certificate2> certs = await searcher.SearchAsync(Settings.Filter, cancellationToken).ConfigureAwait(false);
cert = certs.FirstOrDefault();
IReadOnlyList<X509Certificate2> searchResult = await searcher.SearchAsync(Settings.Filter, cancellationToken).ConfigureAwait(false);
certs.AddRange(searchResult);
}
else
{
@@ -52,42 +53,48 @@ public sealed class GetCertificateInfoJob : Job<GetCertificateInfoSettings>
CLog.Error("Either file or cert-store must be specified when requesting info on certificate");
}
if (cert != null)
if (certs.Count > 0)
{
using (StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(256))
using (StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(512 * certs.Count))
{
int padSize = 24;
StringBuilder sb = lease.Builder;
sb.AppendLine();
AppendWithPadding(sb, "Subject", cert.Subject.StartsWith("CN=") ? cert.Subject.Substring(3) : cert.Subject, padSize);
AppendWithPadding(sb, "Issuer", cert.Issuer.StartsWith("CN=") ? cert.Issuer.Substring(3) : cert.Issuer, padSize);
AppendWithPadding(sb, "Not Before", cert.NotBefore.ToString("s"), padSize);
AppendWithPadding(sb, "Not After", cert.NotAfter.ToString("s"), padSize);
AppendWithPadding(sb, "Signature Algorithm", cert.SignatureAlgorithm.FriendlyName, padSize);
AppendWithPadding(sb, "Thumbprint", cert.Thumbprint, padSize);
if (!string.IsNullOrEmpty(cert.FriendlyName))
foreach (X509Certificate2 cert in certs)
{
AppendWithPadding(sb, "FriendlyName", cert.FriendlyName, padSize);
}
foreach (X509Extension ext in cert.Extensions)
{
string[] values = ext.Format(true).TrimEnd(Environment.NewLine.ToCharArray()).Split(Environment.NewLine);
if ((values.Length > 1 && !string.IsNullOrEmpty(values[values.Length - 1])) || values.Length > 2)
int padSize = 24;
sb.AppendLine();
AppendWithPadding(sb, "Subject", cert.Subject.StartsWith("CN=") ? cert.Subject.Substring(3) : cert.Subject, padSize);
AppendWithPadding(sb, "Issuer", cert.Issuer.StartsWith("CN=") ? cert.Issuer.Substring(3) : cert.Issuer, padSize);
AppendWithPadding(sb, "Not Before", cert.NotBefore.ToString("s"), padSize);
AppendWithPadding(sb, "Not After", cert.NotAfter.ToString("s"), padSize);
AppendWithPadding(sb, "Signature Algorithm", cert.SignatureAlgorithm.FriendlyName, padSize);
AppendWithPadding(sb, "Thumbprint", cert.Thumbprint, padSize);
if (!string.IsNullOrEmpty(cert.FriendlyName))
{
AppendWithPadding(sb, ext.Oid?.FriendlyName ?? ext.Oid?.Value, string.Empty, padSize);
foreach (string value in values)
AppendWithPadding(sb, "FriendlyName", cert.FriendlyName, padSize);
}
foreach (X509Extension ext in cert.Extensions)
{
string[] values = ext.Format(true).TrimEnd(Environment.NewLine.ToCharArray()).Split(Environment.NewLine);
if ((values.Length > 1 && !string.IsNullOrEmpty(values[values.Length - 1])) || values.Length > 2)
{
if (string.IsNullOrEmpty(value))
AppendWithPadding(sb, ext.Oid?.FriendlyName ?? ext.Oid?.Value, string.Empty, padSize);
foreach (string value in values)
{
continue;
if (string.IsNullOrEmpty(value))
{
continue;
}
sb.AppendFormatLine(" - {0}", value);
}
sb.AppendFormatLine(" - {0}", value);
}
else
{
AppendWithPadding(sb, ext.Oid?.FriendlyName ?? ext.Oid?.Value, values[0], padSize);
}
}
else
{
AppendWithPadding(sb, ext.Oid?.FriendlyName ?? ext.Oid?.Value, values[0], padSize);
}
sb.Append("----------------------------------------------------------------");
}
return CreateSuccess(sb.ToString());

View File

@@ -14,7 +14,7 @@ internal static class Program
args = [
"--job=get-certificate-info",
"--cert-store=user/my",
"--filter=thumbprint:b395149b6079553eea92d9a73ffc97463e8976ff"
"--filter=thumbprint:regex:b39.+"
];
// args = [